From 0b0890a07d8a0f7d87e1d871ff957bfcfe490c8c Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 16:39:25 +0100 Subject: [PATCH 01/37] Added package cleanup project --- .gitea/build dev.yaml | 66 +++++++++++++++++++ .gitignore | 5 ++ package-cleanup/action.yaml | 19 ++++++ sh.actions.sln | 16 +++++ .../sh.actions.package-cleanup/Program.cs | 2 + .../sh.actions.package-cleanup.csproj | 30 +++++++++ 6 files changed, 138 insertions(+) create mode 100644 .gitea/build dev.yaml create mode 100644 .gitignore create mode 100644 package-cleanup/action.yaml create mode 100644 sh.actions.sln create mode 100644 sh.actions/sh.actions.package-cleanup/Program.cs create mode 100644 sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj diff --git a/.gitea/build dev.yaml b/.gitea/build dev.yaml new file mode 100644 index 0000000..6623039 --- /dev/null +++ b/.gitea/build dev.yaml @@ -0,0 +1,66 @@ +name: Build on push +run-name: Build on push +on: + push: + branches: + - dev + +jobs: + prepare: + runs-on: [runner] + container: git.sh-edraft.de/sh-edraft.de/act-runner:latest + steps: + - uses: https://git.sh-edraft.de/sh-edraft.de/actions/set-version@v1 + with: + version_suffix: dev + env: + CI_ACCESS_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }} + + build: + runs-on: [runner] + needs: prepare + container: git.sh-edraft.de/sh-edraft.de/act-runner:latest + steps: + - name: Clone Repository + uses: https://github.com/actions/checkout@v3 + with: + token: ${{ secrets.CI_ACCESS_TOKEN }} + + - name: Download build version artifact + uses: actions/download-artifact@v3 + with: + name: version + + - name: Build package + run: | + cd sh.actions/sh.actions.package-cleanup + dotnet build -c Release -p:Version=$(cat ../../version.txt) + + - name: Configure nuget + run: | + dotnet nuget add source --name git_sh-edraft_de --username ${{ secrets.CI_USERNAME }} --password ${{ secrets.CI_ACCESS_TOKEN }} --store-password-in-clear-text https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json + + - name: Upload + run: | + cd sh.actions/sh.actions.package-cleanup + VERSION_FIXED=$(cat ../../version.txt | sed 's/\.0\([0-9]\)/.\1/g' | sed 's/\.0-/-/g') + dotnet nuget push --source git_sh-edraft_de sh.actions.package-cleanup/bin/Release/sh.actions.package-cleanup.$VERSION_FIXED.nupkg + + test: + runs-on: [runner] + needs: build + container: git.sh-edraft.de/sh-edraft.de/act-runner:latest + steps: + - name: Install set-version tool + shell: bash + run: | + dotnet tool install \ + --tool-path .tools \ + package-cleanup \ + --version 1.0.0 \ + --add-source https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json + + - name: Run set-version + shell: bash + run: | + ./.tools/package-cleanup diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9585fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea +.code + +bin/ +obj/ \ No newline at end of file diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml new file mode 100644 index 0000000..dac5a53 --- /dev/null +++ b/package-cleanup/action.yaml @@ -0,0 +1,19 @@ +name: "package cleanup" +description: "Cleans up old packages and versions" + +runs: + using: "composite" + steps: + - name: Install set-version tool + shell: bash + run: | + dotnet tool install \ + --tool-path .tools \ + ShEdraft.SetVersion \ + --version 1.0.0 \ + --add-source https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json + + - name: Run set-version + shell: bash + run: | + ./.tools/set-version --suffix dev diff --git a/sh.actions.sln b/sh.actions.sln new file mode 100644 index 0000000..2e8a884 --- /dev/null +++ b/sh.actions.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "sh.actions.package-cleanup", "sh.actions/sh.actions.package-cleanup\sh.actions.package-cleanup.csproj", "{F4995B6A-2CA1-4CD4-AEAC-BE397AF1910A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F4995B6A-2CA1-4CD4-AEAC-BE397AF1910A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4995B6A-2CA1-4CD4-AEAC-BE397AF1910A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4995B6A-2CA1-4CD4-AEAC-BE397AF1910A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4995B6A-2CA1-4CD4-AEAC-BE397AF1910A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/sh.actions/sh.actions.package-cleanup/Program.cs b/sh.actions/sh.actions.package-cleanup/Program.cs new file mode 100644 index 0000000..3751555 --- /dev/null +++ b/sh.actions/sh.actions.package-cleanup/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj b/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj new file mode 100644 index 0000000..8428a61 --- /dev/null +++ b/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj @@ -0,0 +1,30 @@ + + + + Exe + net10.0 + sh.actions.package_cleanup + enable + enable + + + true + package-cleanup + + sh.actions.package-cleanup + 0.0.1 + sh-edraft gitea package cleanup + Tool to cleanup gitea packages + edraft + https://git.sh-edraft.de/sh-edraft.de/actions + https://git.sh-edraft.de/sh-edraft.de/actions + git + MIT + false + true + true + snupkg + README.md + + + From e5eb38507b4cc0a39511df56cf23d51b20fd962b Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 16:40:09 +0100 Subject: [PATCH 02/37] trigger ci --- .gitea/{ => workflows}/build dev.yaml | 0 sh.actions/sh.actions.package-cleanup/Program.cs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename .gitea/{ => workflows}/build dev.yaml (100%) diff --git a/.gitea/build dev.yaml b/.gitea/workflows/build dev.yaml similarity index 100% rename from .gitea/build dev.yaml rename to .gitea/workflows/build dev.yaml diff --git a/sh.actions/sh.actions.package-cleanup/Program.cs b/sh.actions/sh.actions.package-cleanup/Program.cs index 3751555..cb1afde 100644 --- a/sh.actions/sh.actions.package-cleanup/Program.cs +++ b/sh.actions/sh.actions.package-cleanup/Program.cs @@ -1,2 +1,2 @@ // See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +Console.WriteLine("Hello, World! from ci"); From b503de71b49dc7a70c34ded1712212178db68b82 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 16:41:54 +0100 Subject: [PATCH 03/37] fixed build --- .gitea/workflows/build dev.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index 6623039..f16e882 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -16,11 +16,11 @@ jobs: env: CI_ACCESS_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }} - build: - runs-on: [runner] - needs: prepare - container: git.sh-edraft.de/sh-edraft.de/act-runner:latest - steps: + build: + runs-on: [runner] + needs: prepare + container: git.sh-edraft.de/sh-edraft.de/act-runner:latest + steps: - name: Clone Repository uses: https://github.com/actions/checkout@v3 with: From bcfbbfc470bd4f0b41fbb65bcf71ddaed7585fcd Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 16:55:11 +0100 Subject: [PATCH 04/37] Added readme --- sh.actions/sh.actions.package-cleanup/README.md | 1 + .../sh.actions.package-cleanup.csproj | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 sh.actions/sh.actions.package-cleanup/README.md diff --git a/sh.actions/sh.actions.package-cleanup/README.md b/sh.actions/sh.actions.package-cleanup/README.md new file mode 100644 index 0000000..07f0087 --- /dev/null +++ b/sh.actions/sh.actions.package-cleanup/README.md @@ -0,0 +1 @@ +# Gitea package cleanup \ No newline at end of file diff --git a/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj b/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj index 8428a61..f5131f1 100644 --- a/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj +++ b/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj @@ -27,4 +27,8 @@ README.md + + + + From 9e25b8c4a1654df0fc4e994ab78a7e99f667a9d5 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 16:56:38 +0100 Subject: [PATCH 05/37] Fixed build --- .gitea/workflows/build dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index f16e882..b5ab7c8 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -44,7 +44,7 @@ jobs: run: | cd sh.actions/sh.actions.package-cleanup VERSION_FIXED=$(cat ../../version.txt | sed 's/\.0\([0-9]\)/.\1/g' | sed 's/\.0-/-/g') - dotnet nuget push --source git_sh-edraft_de sh.actions.package-cleanup/bin/Release/sh.actions.package-cleanup.$VERSION_FIXED.nupkg + dotnet nuget push --source git_sh-edraft_de bin/Release/sh.actions.package-cleanup.$VERSION_FIXED.nupkg test: runs-on: [runner] From f3cff587074cde0b82edcebd808dedf3efe73a18 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 16:58:03 +0100 Subject: [PATCH 06/37] Fixed tool install --- .gitea/workflows/build dev.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index b5ab7c8..26d63f6 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -51,16 +51,16 @@ jobs: needs: build container: git.sh-edraft.de/sh-edraft.de/act-runner:latest steps: - - name: Install set-version tool + - name: Install package-cleanup tool shell: bash run: | dotnet tool install \ --tool-path .tools \ - package-cleanup \ + sh.actions.package-cleanup \ --version 1.0.0 \ --add-source https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json - - name: Run set-version + - name: Run package-cleanup shell: bash run: | ./.tools/package-cleanup From c6465b70d1c51baf2cd403348ea89d74b4021eb0 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 17:08:22 +0100 Subject: [PATCH 07/37] test local action --- .gitea/workflows/build dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index 26d63f6..ddc5111 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -10,7 +10,7 @@ jobs: runs-on: [runner] container: git.sh-edraft.de/sh-edraft.de/act-runner:latest steps: - - uses: https://git.sh-edraft.de/sh-edraft.de/actions/set-version@v1 + - uses: ./set-version/action.yaml with: version_suffix: dev env: From 008de6593ca790a78a9b4814333258df4be0e172 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 17:13:07 +0100 Subject: [PATCH 08/37] Refixed action --- .gitea/workflows/build dev.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index ddc5111..87011af 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -10,7 +10,7 @@ jobs: runs-on: [runner] container: git.sh-edraft.de/sh-edraft.de/act-runner:latest steps: - - uses: ./set-version/action.yaml + - uses: https://git.sh-edraft.de/sh-edraft.de/actions/set-version@v1 with: version_suffix: dev env: @@ -56,7 +56,7 @@ jobs: run: | dotnet tool install \ --tool-path .tools \ - sh.actions.package-cleanup \ + package-cleanup \ --version 1.0.0 \ --add-source https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json From 22d0361afe1c7dd2fa1fa32a6fee7754456c212c Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 17:15:07 +0100 Subject: [PATCH 09/37] Fixed install --- .gitea/workflows/build dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index 87011af..26d63f6 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -56,7 +56,7 @@ jobs: run: | dotnet tool install \ --tool-path .tools \ - package-cleanup \ + sh.actions.package-cleanup \ --version 1.0.0 \ --add-source https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json From a06557dc4899bbe80aa16625a18cb2f066b2ae39 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 17:17:27 +0100 Subject: [PATCH 10/37] use local action --- .gitea/workflows/build dev.yaml | 3 +-- .../Service/GiteaPackageService.cs | 6 ++++++ .../Service/IGiteaPackageService.cs | 6 ++++++ 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs create mode 100644 sh.actions/sh.actions.package-cleanup/Service/IGiteaPackageService.cs diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index 26d63f6..756c047 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -10,7 +10,7 @@ jobs: runs-on: [runner] container: git.sh-edraft.de/sh-edraft.de/act-runner:latest steps: - - uses: https://git.sh-edraft.de/sh-edraft.de/actions/set-version@v1 + - uses: ./actions/set-version with: version_suffix: dev env: @@ -57,7 +57,6 @@ jobs: dotnet tool install \ --tool-path .tools \ sh.actions.package-cleanup \ - --version 1.0.0 \ --add-source https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json - name: Run package-cleanup diff --git a/sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs b/sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs new file mode 100644 index 0000000..6594406 --- /dev/null +++ b/sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs @@ -0,0 +1,6 @@ +namespace sh.actions.package_cleanup.Service; + +public class GiteaPackageService : IGiteaPackageService +{ + +} \ No newline at end of file diff --git a/sh.actions/sh.actions.package-cleanup/Service/IGiteaPackageService.cs b/sh.actions/sh.actions.package-cleanup/Service/IGiteaPackageService.cs new file mode 100644 index 0000000..3ec0b5a --- /dev/null +++ b/sh.actions/sh.actions.package-cleanup/Service/IGiteaPackageService.cs @@ -0,0 +1,6 @@ +namespace sh.actions.package_cleanup.Service; + +public interface IGiteaPackageService +{ + +} \ No newline at end of file From 0f48552783a4ea735a4a05e92b47240d4f92d662 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 17:26:55 +0100 Subject: [PATCH 11/37] Use local action --- .gitea/workflows/build dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index 756c047..e96836c 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -10,7 +10,7 @@ jobs: runs-on: [runner] container: git.sh-edraft.de/sh-edraft.de/act-runner:latest steps: - - uses: ./actions/set-version + - uses: https://git.sh-edraft.de/sh-edraft.de/actions/set-version@dev with: version_suffix: dev env: From fef6b580e38d5500452b5b6f71255a5c6b636b71 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 17:42:04 +0100 Subject: [PATCH 12/37] Auth for nuget tooling --- .gitea/workflows/build dev.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index e96836c..317771c 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -51,13 +51,17 @@ jobs: needs: build container: git.sh-edraft.de/sh-edraft.de/act-runner:latest steps: + - name: Configure nuget + run: | + dotnet nuget add source --name git_sh-edraft_de --username ${{ secrets.CI_USERNAME }} --password ${{ secrets.CI_ACCESS_TOKEN }} --store-password-in-clear-text https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json + - name: Install package-cleanup tool shell: bash run: | dotnet tool install \ --tool-path .tools \ sh.actions.package-cleanup \ - --add-source https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json + --source sh-git_sh-edraft_de - name: Run package-cleanup shell: bash From c19921eb852111b394d80dd118ab5013a59f69d5 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 17:45:35 +0100 Subject: [PATCH 13/37] Try fix install --- .gitea/workflows/build dev.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index 317771c..6fb42c5 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -60,10 +60,10 @@ jobs: run: | dotnet tool install \ --tool-path .tools \ - sh.actions.package-cleanup \ + package-cleanup \ --source sh-git_sh-edraft_de - name: Run package-cleanup shell: bash run: | - ./.tools/package-cleanup + ./.tools/sh.actions.package-cleanup From 0332f08194364716ebf3005df66a6fb65073f4a8 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 17:56:57 +0100 Subject: [PATCH 14/37] Try generic upload --- .gitea/workflows/build dev.yaml | 57 ++++++++-------- .../Models/GiteaConfig.cs | 23 +++++++ .../Models/GiteaPackage.cs | 10 +++ .../sh.actions.package-cleanup/Program.cs | 65 ++++++++++++++++++- .../sh.actions.package-cleanup/README.md | 1 - .../Service/GiteaPackageService.cs | 46 ++++++++++++- .../Service/IGiteaPackageService.cs | 4 +- .../sh.actions.package-cleanup.csproj | 35 +++++----- 8 files changed, 190 insertions(+), 51 deletions(-) create mode 100644 sh.actions/sh.actions.package-cleanup/Models/GiteaConfig.cs create mode 100644 sh.actions/sh.actions.package-cleanup/Models/GiteaPackage.cs delete mode 100644 sh.actions/sh.actions.package-cleanup/README.md diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index 6fb42c5..b5563cf 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -31,39 +31,42 @@ jobs: with: name: version - - name: Build package + - name: Build single file executables run: | cd sh.actions/sh.actions.package-cleanup - dotnet build -c Release -p:Version=$(cat ../../version.txt) - - name: Configure nuget - run: | - dotnet nuget add source --name git_sh-edraft_de --username ${{ secrets.CI_USERNAME }} --password ${{ secrets.CI_ACCESS_TOKEN }} --store-password-in-clear-text https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json + # Build for Linux x64 + dotnet publish -c Release -r linux-x64 -p:Version=$(cat ../../version.txt) -o publish/linux-x64 - - name: Upload + - name: Upload to Gitea Generic Package Registry run: | cd sh.actions/sh.actions.package-cleanup - VERSION_FIXED=$(cat ../../version.txt | sed 's/\.0\([0-9]\)/.\1/g' | sed 's/\.0-/-/g') - dotnet nuget push --source git_sh-edraft_de bin/Release/sh.actions.package-cleanup.$VERSION_FIXED.nupkg + curl -X PUT \ + -H "Authorization: token ${{ secrets.CI_ACCESS_TOKEN }}" \ + -T publish/linux-x64/sh.actions.package-cleanup \ + "https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/$(cat ../../version.txt)/package-cleanup-linux-x64" - test: - runs-on: [runner] - needs: build - container: git.sh-edraft.de/sh-edraft.de/act-runner:latest - steps: - - name: Configure nuget - run: | - dotnet nuget add source --name git_sh-edraft_de --username ${{ secrets.CI_USERNAME }} --password ${{ secrets.CI_ACCESS_TOKEN }} --store-password-in-clear-text https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json + # test: + # runs-on: [runner] + # needs: build + # container: git.sh-edraft.de/sh-edraft.de/act-runner:latest + # steps: + # - name: Download and test package-cleanup tool + # shell: bash + # run: | + # # Create tools directory + # mkdir -p .tools - - name: Install package-cleanup tool - shell: bash - run: | - dotnet tool install \ - --tool-path .tools \ - package-cleanup \ - --source sh-git_sh-edraft_de + # # Download the Linux x64 version (since we're running on Linux) + # VERSION=$(curl -s https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup | jq -r '.versions[0].name') + # curl -L -H "Authorization: token ${{ secrets.CI_ACCESS_TOKEN }}" \ + # -o .tools/package-cleanup \ + # "https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/$VERSION/package-cleanup-linux-x64" - - name: Run package-cleanup - shell: bash - run: | - ./.tools/sh.actions.package-cleanup + # # Make executable + # chmod +x .tools/package-cleanup + + # - name: Run package-cleanup + # shell: bash + # run: | + # ./.tools/package-cleanup diff --git a/sh.actions/sh.actions.package-cleanup/Models/GiteaConfig.cs b/sh.actions/sh.actions.package-cleanup/Models/GiteaConfig.cs new file mode 100644 index 0000000..ae76675 --- /dev/null +++ b/sh.actions/sh.actions.package-cleanup/Models/GiteaConfig.cs @@ -0,0 +1,23 @@ +namespace sh.actions.package_cleanup.Models; + +/// +/// Konfiguration für Gitea-Verbindung +/// +public class GiteaConfig +{ + /// + /// Basis-URL der Gitea-Instanz (z.B. https://git.example.com) + /// + public string GiteaUrl { get; set; } = string.Empty; + + /// + /// API-Token für Authentifizierung + /// + public string ApiToken { get; set; } = string.Empty; + + /// + /// Owner/Username des Pakets + /// + public string Owner { get; set; } = string.Empty; +} + diff --git a/sh.actions/sh.actions.package-cleanup/Models/GiteaPackage.cs b/sh.actions/sh.actions.package-cleanup/Models/GiteaPackage.cs new file mode 100644 index 0000000..d72720f --- /dev/null +++ b/sh.actions/sh.actions.package-cleanup/Models/GiteaPackage.cs @@ -0,0 +1,10 @@ +namespace sh.actions.package_cleanup.Models; + +public class GiteaPackage +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/sh.actions/sh.actions.package-cleanup/Program.cs b/sh.actions/sh.actions.package-cleanup/Program.cs index cb1afde..05d239c 100644 --- a/sh.actions/sh.actions.package-cleanup/Program.cs +++ b/sh.actions/sh.actions.package-cleanup/Program.cs @@ -1,2 +1,65 @@ // See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World! from ci"); +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using sh.actions.package_cleanup.Models; +using sh.actions.package_cleanup.Service; + +Console.WriteLine("Hello, World!"); +// +// // Setup Dependency Injection +// var services = new ServiceCollection(); +// +// // Add Logging +// services.AddLogging(builder => +// builder.AddConsole() +// .SetMinimumLevel(LogLevel.Information) +// ); +// +// // Load configuration from environment variables or appsettings +// var giteaUrl = Environment.GetEnvironmentVariable("GITEA_URL") ?? "https://git.example.com"; +// var apiToken = Environment.GetEnvironmentVariable("GITEA_API_TOKEN") ?? string.Empty; +// var owner = Environment.GetEnvironmentVariable("GITEA_OWNER") ?? "sh-edraft"; +// +// var giteaConfig = new GiteaConfig +// { +// GiteaUrl = giteaUrl, +// ApiToken = apiToken, +// Owner = owner +// }; +// +// services.AddSingleton(giteaConfig); +// +// // Add HttpClient +// services.AddHttpClient(); +// +// // Build service provider +// var serviceProvider = services.BuildServiceProvider(); +// +// try +// { +// // Get the service and execute +// var packageService = serviceProvider.GetRequiredService(); +// var logger = serviceProvider.GetRequiredService>(); +// +// logger.LogInformation("Starting package cleanup tool"); +// logger.LogInformation("Gitea URL: {GiteaUrl}", giteaUrl); +// logger.LogInformation("Owner: {Owner}", owner); +// +// using var cts = new CancellationTokenSource(); +// var packages = await packageService.GetPackagesByOwnerAsync(cts.Token); +// +// logger.LogInformation("Retrieved {Count} packages", packages.Count()); +// +// foreach (var package in packages) +// { +// logger.LogInformation("Package: {Name} (Version: {Version})", package.Name, package.Version); +// } +// +// logger.LogInformation("Package cleanup tool completed successfully"); +// } +// catch (Exception ex) +// { +// var logger = serviceProvider.GetRequiredService>(); +// logger.LogError(ex, "Application error"); +// Environment.Exit(1); +// } diff --git a/sh.actions/sh.actions.package-cleanup/README.md b/sh.actions/sh.actions.package-cleanup/README.md deleted file mode 100644 index 07f0087..0000000 --- a/sh.actions/sh.actions.package-cleanup/README.md +++ /dev/null @@ -1 +0,0 @@ -# Gitea package cleanup \ No newline at end of file diff --git a/sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs b/sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs index 6594406..bd13abe 100644 --- a/sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs +++ b/sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs @@ -1,6 +1,48 @@ namespace sh.actions.package_cleanup.Service; -public class GiteaPackageService : IGiteaPackageService +using System.Net.Http.Json; +using Microsoft.Extensions.Logging; +using sh.actions.package_cleanup.Models; + +public class GiteaPackageService(HttpClient httpClient, GiteaConfig config, ILogger logger) + : IGiteaPackageService { - + public async Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default) + { + try + { + var url = $"{config.GiteaUrl.TrimEnd('/')}/api/v1/packages/{config.Owner}"; + + logger.LogInformation("Fetching packages from Gitea: {Url}", url); + + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Add authentication if token is provided + if (!string.IsNullOrEmpty(config.ApiToken)) + { + request.Headers.Add("Authorization", $"token {config.ApiToken}"); + } + + var response = await httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + + var packages = + await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken) + ?? new List(); + + logger.LogInformation("Successfully fetched {Count} packages", packages.Count); + + return packages; + } + catch (HttpRequestException ex) + { + logger.LogError(ex, "Error fetching packages from Gitea"); + throw; + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected error fetching packages"); + throw; + } + } } \ No newline at end of file diff --git a/sh.actions/sh.actions.package-cleanup/Service/IGiteaPackageService.cs b/sh.actions/sh.actions.package-cleanup/Service/IGiteaPackageService.cs index 3ec0b5a..bb3017e 100644 --- a/sh.actions/sh.actions.package-cleanup/Service/IGiteaPackageService.cs +++ b/sh.actions/sh.actions.package-cleanup/Service/IGiteaPackageService.cs @@ -1,6 +1,8 @@ namespace sh.actions.package_cleanup.Service; +using sh.actions.package_cleanup.Models; + public interface IGiteaPackageService { - + Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj b/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj index f5131f1..038a930 100644 --- a/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj +++ b/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj @@ -6,29 +6,26 @@ sh.actions.package_cleanup enable enable - - - true - package-cleanup - - sh.actions.package-cleanup 0.0.1 - sh-edraft gitea package cleanup - Tool to cleanup gitea packages - edraft - https://git.sh-edraft.de/sh-edraft.de/actions - https://git.sh-edraft.de/sh-edraft.de/actions - git - MIT - false - true - true - snupkg - README.md + + + true + true + true + true + full + + - + + + + + + + From 5e493513f1ea059261c6b36939debf3fdf1bf81d Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 18:00:45 +0100 Subject: [PATCH 15/37] ich hab kein bock mehr --- .gitea/workflows/build dev.yaml | 37 +++++++++++++-------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index b5563cf..1d59e87 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -46,27 +46,20 @@ jobs: -T publish/linux-x64/sh.actions.package-cleanup \ "https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/$(cat ../../version.txt)/package-cleanup-linux-x64" - # test: - # runs-on: [runner] - # needs: build - # container: git.sh-edraft.de/sh-edraft.de/act-runner:latest - # steps: - # - name: Download and test package-cleanup tool - # shell: bash - # run: | - # # Create tools directory - # mkdir -p .tools + test: + runs-on: [runner] + needs: build + container: git.sh-edraft.de/sh-edraft.de/act-runner:latest + steps: + - name: Download and test package-cleanup tool + shell: bash + run: | + curl -OJ https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/package-cleanup-linux-x64 - # # Download the Linux x64 version (since we're running on Linux) - # VERSION=$(curl -s https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup | jq -r '.versions[0].name') - # curl -L -H "Authorization: token ${{ secrets.CI_ACCESS_TOKEN }}" \ - # -o .tools/package-cleanup \ - # "https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/$VERSION/package-cleanup-linux-x64" + # Make executable + chmod +x package-cleanup-linux-x64 - # # Make executable - # chmod +x .tools/package-cleanup - - # - name: Run package-cleanup - # shell: bash - # run: | - # ./.tools/package-cleanup + - name: Run package-cleanup + shell: bash + run: | + ./package-cleanup-linux-x64 From 98abbe661c70189f0dad3c22fbd7bffc6626f767 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 20:48:34 +0100 Subject: [PATCH 16/37] Added package filtering --- .gitea/workflows/build dev.yaml | 18 - .gitignore | 9 +- .../Service/PackageServiceDateVersionTests.cs | 617 ++++++++++++++++++ .../PackageServiceSemanticVersionTests.cs | 498 ++++++++++++++ .../PackageServiceVersionDetectionTests.cs | 349 ++++++++++ .../sh.actions.package-cleanup.Tests.csproj | 26 + .../ConfigurationExtension.cs | 26 + .../Models/GiteaPackage.cs | 0 sh.actions.package-cleanup/Program.cs | 21 + .../Properties/launchSettings.json | 12 + .../Service/GiteaPackageService.cs | 95 +++ .../Service/IGiteaPackageService.cs | 1 + .../Service/IPackageService.cs | 9 + .../Service/PackageService.cs | 311 +++++++++ sh.actions.package-cleanup/Worker.cs | 41 ++ sh.actions.package-cleanup/appsettings.json | 1 + .../sh.actions.package-cleanup.csproj | 15 + sh.actions.sln | 42 +- sh.actions.sln.DotSettings.user | 7 + .../Models/GiteaConfig.cs | 23 - .../sh.actions.package-cleanup/Program.cs | 65 -- .../Service/GiteaPackageService.cs | 48 -- .../sh.actions.package-cleanup.csproj | 31 - 23 files changed, 2072 insertions(+), 193 deletions(-) create mode 100644 sh.actions.package-cleanup.Tests/Service/PackageServiceDateVersionTests.cs create mode 100644 sh.actions.package-cleanup.Tests/Service/PackageServiceSemanticVersionTests.cs create mode 100644 sh.actions.package-cleanup.Tests/Service/PackageServiceVersionDetectionTests.cs create mode 100644 sh.actions.package-cleanup.Tests/sh.actions.package-cleanup.Tests.csproj create mode 100644 sh.actions.package-cleanup/ConfigurationExtension.cs rename {sh.actions/sh.actions.package-cleanup => sh.actions.package-cleanup}/Models/GiteaPackage.cs (100%) create mode 100644 sh.actions.package-cleanup/Program.cs create mode 100644 sh.actions.package-cleanup/Properties/launchSettings.json create mode 100644 sh.actions.package-cleanup/Service/GiteaPackageService.cs rename {sh.actions/sh.actions.package-cleanup => sh.actions.package-cleanup}/Service/IGiteaPackageService.cs (71%) create mode 100644 sh.actions.package-cleanup/Service/IPackageService.cs create mode 100644 sh.actions.package-cleanup/Service/PackageService.cs create mode 100644 sh.actions.package-cleanup/Worker.cs create mode 100644 sh.actions.package-cleanup/appsettings.json create mode 100644 sh.actions.package-cleanup/sh.actions.package-cleanup.csproj create mode 100644 sh.actions.sln.DotSettings.user delete mode 100644 sh.actions/sh.actions.package-cleanup/Models/GiteaConfig.cs delete mode 100644 sh.actions/sh.actions.package-cleanup/Program.cs delete mode 100644 sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs delete mode 100644 sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index 1d59e87..22734e7 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -45,21 +45,3 @@ jobs: -H "Authorization: token ${{ secrets.CI_ACCESS_TOKEN }}" \ -T publish/linux-x64/sh.actions.package-cleanup \ "https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/$(cat ../../version.txt)/package-cleanup-linux-x64" - - test: - runs-on: [runner] - needs: build - container: git.sh-edraft.de/sh-edraft.de/act-runner:latest - steps: - - name: Download and test package-cleanup tool - shell: bash - run: | - curl -OJ https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/package-cleanup-linux-x64 - - # Make executable - chmod +x package-cleanup-linux-x64 - - - name: Run package-cleanup - shell: bash - run: | - ./package-cleanup-linux-x64 diff --git a/.gitignore b/.gitignore index b9585fb..dd3c5ba 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,11 @@ .code bin/ -obj/ \ No newline at end of file +obj/ + +# Environment Variables - DO NOT COMMIT +.env +.env.local +*.env.local + +**/appsettings.local.json diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceDateVersionTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceDateVersionTests.cs new file mode 100644 index 0000000..8f45fcb --- /dev/null +++ b/sh.actions.package-cleanup.Tests/Service/PackageServiceDateVersionTests.cs @@ -0,0 +1,617 @@ +using Microsoft.Extensions.Configuration; +using Moq; +using sh.actions.package_cleanup.Models; +using sh.actions.package_cleanup.Service; +using Xunit; + +namespace sh.actions.package_cleanup.Tests.Service; + +public class PackageServiceDateVersionTests +{ + private Mock _configurationMock; + private Mock _giteaPackageServiceMock; + private IPackageService _packageService; + + private void SetupMocks() + { + _configurationMock = new Mock(); + _giteaPackageServiceMock = new Mock(); + _packageService = new PackageService(_configurationMock.Object, _giteaPackageServiceMock.Object); + } + + // ...existing code... + + #region Version Detection Tests + + [Theory] + [InlineData("2024.12.25.1")] + [InlineData("2025.2.14.1")] + [InlineData("2026.1.1.100")] + [InlineData("2023.12.31.999")] + public void FilterPackagesToDelete_WithDateVersions_ShouldIdentifyCorrectly(string version) + { + // Arrange + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Single prod date version should not be deleted + Assert.NotNull(result); + Assert.Empty(result); + } + + [Theory] + [InlineData("2024.12.25.1-dev")] + [InlineData("2025.2.14.1-alpha")] + [InlineData("2026.1.1.100-beta")] + [InlineData("2023.12.31.999-rc")] + public void FilterPackagesToDelete_WithDateVersionsAndSuffixes_ShouldIdentifyCorrectly(string version) + { + // Arrange + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Single prerelease/dev date version should be kept + Assert.NotNull(result); + Assert.Empty(result); + } + + #endregion + + #region Prod Release Tests - Date Versioning + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithOnlyProdReleases_ShouldReturnEmpty() + { + // Arrange - CLEAN_PROD_VERSIONS not set, defaults to false + SetupMocks(); + var packages = new List + { + new() + { + Id = 1, Name = "test", Version = "2025.12.20.1", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-30) + }, + new() + { + Id = 2, Name = "test", Version = "2025.12.25.1", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-20) + }, + new() + { + Id = 3, Name = "test", Version = "2026.1.5.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) + } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Prod releases should be protected by default (CLEAN_PROD_VERSIONS=false) + Assert.Empty(result); + } + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithProdReleasesAndCleaningEnabled_ShouldDeleteOldest() + { + // Arrange + SetupMocks(); + _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("true"); + _configurationMock.Setup(c => c["KEEP_PROD_VERSIONS"]).Returns("2"); + + var packages = new List + { + new() + { + Id = 1, Name = "test", Version = "2025.1.1.1", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-365) + }, + new() + { + Id = 2, Name = "test", Version = "2025.6.15.1", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-200) + }, + new() + { + Id = 3, Name = "test", Version = "2026.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) + }, + new() { Id = 4, Name = "test", Version = "2026.2.14.1", Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should delete oldest 2 (2025.1.1 and 2025.6.15), keep newest 2 + Assert.Equal(2, result.Count); + Assert.Contains(result, p => p.Id == 1); + Assert.Contains(result, p => p.Id == 2); + Assert.DoesNotContain(result, p => p.Id == 3 || p.Id == 4); + } + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithMultipleProdVersions_ShouldKeepNewest() + { + // Arrange + SetupMocks(); + var packages = new List + { + new() + { + Id = 1, Name = "test", Version = "2025.1.1.1", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-365) + }, + new() + { + Id = 2, Name = "test", Version = "2025.6.15.1", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-200) + }, + new() + { + Id = 3, Name = "test", Version = "2026.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) + }, + new() { Id = 4, Name = "test", Version = "2026.2.14.1", Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Default is CLEAN_PROD_VERSIONS=false, so nothing should be deleted + Assert.Empty(result); + } + + #endregion + + #region Prerelease Tests - Date Versioning + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithPrereleaseVersions_ShouldKeepLatest() + { + // Arrange - Default KEEP_PRERELEASE_VERSIONS=3 + SetupMocks(); + var packages = new List + { + new() + { + Id = 1, Name = "test", Version = "2026.2.10.1-alpha", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-5) + }, + new() + { + Id = 2, Name = "test", Version = "2026.2.11.1-beta", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-4) + }, + new() + { + Id = 3, Name = "test", Version = "2026.2.12.1-rc", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-3) + }, + new() + { + Id = 4, Name = "test", Version = "2026.2.13.1-preview", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-2) + } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should delete oldest 1 (alpha), keep newest 3 + Assert.Single(result); + Assert.Contains(result, p => p.Id == 1); // alpha is oldest + Assert.DoesNotContain(result, p => p.Id == 2 || p.Id == 3 || p.Id == 4); + } + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithManyPrereleases_ShouldEnforceLimit() + { + // Arrange - Custom KEEP_PRERELEASE_VERSIONS=2 + SetupMocks(); + _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); + + var packages = new List(); + for (int i = 0; i < 10; i++) + { + var date = DateTime.UtcNow.AddDays(-i); + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"{date.Year}.{date.Month}.{date.Day}.{i}-beta", + Type = "docker", + CreatedAt = date + }); + } + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should delete 8 oldest, keep only 2 newest (Id=0,1) + Assert.Equal(8, result.Count); + Assert.DoesNotContain(result, p => p.Id < 2); + Assert.Contains(result, p => p.Id >= 2); + } + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithMixedPrereleases_ShouldOrderByDate() + { + // Arrange + SetupMocks(); + var packages = new List + { + new() + { + Id = 1, Name = "test", Version = "2025.12.1.1-beta", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-60) + }, + new() + { + Id = 2, Name = "test", Version = "2026.1.15.1-beta", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-30) + }, + new() + { + Id = 3, Name = "test", Version = "2026.2.1.1-alpha", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-14) + } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Default keep 3, so all should be kept + Assert.Empty(result); + } + + #endregion + + #region Dev Release Tests - Date Versioning + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithDevVersions_ShouldKeepLatest() + { + // Arrange - Default KEEP_DEV_VERSIONS=5 + SetupMocks(); + var packages = new List + { + new() + { + Id = 1, Name = "test", Version = "2026.2.10.1-dev", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-5) + }, + new() + { + Id = 2, Name = "test", Version = "2026.2.11.2-dev", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-4) + }, + new() + { + Id = 3, Name = "test", Version = "2026.2.12.3-dev", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-3) + } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Default keep 5, so all 3 should be kept + Assert.Empty(result); + } + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithManyDailyDevReleases_ShouldEnforceLimit() + { + // Arrange - Custom KEEP_DEV_VERSIONS=3 + SetupMocks(); + _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("3"); + + var packages = new List(); + for (int i = 0; i < 30; i++) + { + var date = DateTime.UtcNow.AddDays(-i); + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"{date.Year}.{date.Month}.{date.Day}.{i}-dev", + Type = "docker", + CreatedAt = date + }); + } + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should delete 27 oldest, keep only 3 newest (Id=0,1,2) + Assert.Equal(27, result.Count); + Assert.DoesNotContain(result, p => p.Id < 3); + Assert.Contains(result, p => p.Id >= 3); + } + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithManyDailyDevReleasesDefaultLimit_ShouldIdentifyAll() + { + // Arrange + SetupMocks(); + var packages = new List(); + for (int i = 0; i < 30; i++) + { + var date = DateTime.UtcNow.AddDays(-i); + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"{date.Year}.{date.Month}.{date.Day}.{i}-dev", + Type = "docker", + CreatedAt = date + }); + } + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Default keep 5, so 25 should be deleted + Assert.Equal(25, result.Count); + Assert.DoesNotContain(result, p => p.Id < 5); // Keep newest 5 (Id 0-4) + Assert.Contains(result, p => p.Id >= 5); + } + + #endregion + + #region Mixed Release Types - Date Versioning + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithMixedReleaseTypes_ShouldSeparateAndCleanCorrectly() + { + // Arrange + SetupMocks(); + _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); + _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("2"); + + var packages = new List + { + // Prod releases (should not be deleted - CLEAN_PROD_VERSIONS=false) + new() + { + Id = 1, Name = "test", Version = "2025.12.1.1", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-80) + }, + new() + { + Id = 2, Name = "test", Version = "2026.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) + }, + // Prerelease (keep 2 newest) + new() + { + Id = 3, Name = "test", Version = "2026.1.25.1-alpha", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-20) + }, + new() + { + Id = 4, Name = "test", Version = "2026.2.1.1-beta", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-14) + }, + new() + { + Id = 5, Name = "test", Version = "2026.2.5.1-rc", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-10) + }, + // Dev (keep 2 newest) + new() + { + Id = 6, Name = "test", Version = "2026.2.10.1-dev", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-5) + }, + new() { Id = 7, Name = "test", Version = "2026.2.14.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert + Assert.Equal(1, result.Count); // Should delete 1 prerelease (alpha), 0 dev (keep both) + Assert.DoesNotContain(result, p => p.Id <= 2); // Prod not deleted + Assert.DoesNotContain(result, p => p.Id >= 6); // Keep 2 latest dev + } + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithLargeSet_ShouldHandleCorrectly() + { + // Arrange + SetupMocks(); + _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("false"); + _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("3"); + _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("5"); + + var packages = new List(); + + // Add prod releases + for (int i = 0; i < 5; i++) + { + var date = DateTime.UtcNow.AddMonths(-(5 - i)); + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"{date.Year}.{date.Month}.1.1", + Type = "docker", + CreatedAt = date + }); + } + + // Add prerelease + for (int i = 5; i < 15; i++) + { + var date = DateTime.UtcNow.AddDays(-(15 - i) * 7); + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"{date.Year}.{date.Month}.{date.Day}.1-beta", + Type = "docker", + CreatedAt = date + }); + } + + // Add dev + for (int i = 15; i < 30; i++) + { + var date = DateTime.UtcNow.AddDays(-(30 - i)); + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"{date.Year}.{date.Month}.{date.Day}.{(30 - i)}-dev", + Type = "docker", + CreatedAt = date + }); + } + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert + Assert.NotNull(result); + // Prod releases should never be deleted + Assert.DoesNotContain(result, p => p.Id < 5); + // Should delete 7 oldest prerelease (keep 3 newest) + var prereleaseDeleted = result.Where(p => p.Id >= 5 && p.Id < 15).ToList(); + Assert.Equal(7, prereleaseDeleted.Count); + // Should delete 10 oldest dev (keep 5 newest) + var devDeleted = result.Where(p => p.Id >= 15).ToList(); + Assert.Equal(10, devDeleted.Count); + } + + #endregion + + #region Edge Cases - Date Versioning + + [Theory] + [InlineData("1000.1.1.1")] // Minimum valid year + [InlineData("2999.12.31.999")] // Maximum valid year + [InlineData("2025.2.14.0")] // Build number 0 + public void FilterPackagesToDeleteByDateVersion_WithBoundaryVersions_ShouldHandleCorrectly(string version) + { + // Arrange + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert + Assert.NotNull(result); + } + + [Theory] + [InlineData("999.12.31.1")] // Year too small + [InlineData("3000.1.1.1")] // Year too large + [InlineData("2025.13.1.1")] // Month out of range + [InlineData("2025.0.1.1")] // Month zero + [InlineData("2025.2.32.1")] // Day out of range + [InlineData("2025.2.0.1")] // Day zero + public void FilterPackagesToDelete_WithInvalidDateVersions_ShouldNotProcess(string version) + { + // Arrange + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow }, + new() { Id = 2, Name = "test", Version = "1.0.0.1", Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Invalid date version should be skipped, only semantic version processed + Assert.NotNull(result); + } + + [Fact] + public void FilterPackagesToDeleteByDateVersion_WithConsecutiveDates_ShouldOrderCorrectly() + { + // Arrange + SetupMocks(); + var packages = new List + { + new() + { + Id = 1, Name = "test", Version = "2025.2.10.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) + }, + new() + { + Id = 2, Name = "test", Version = "2025.2.11.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) + }, + new() + { + Id = 3, Name = "test", Version = "2025.2.12.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) + }, + new() + { + Id = 4, Name = "test", Version = "2025.2.13.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-2) + }, + new() { Id = 5, Name = "test", Version = "2025.2.14.1", Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert + Assert.NotNull(result); + } + + #endregion + + #region Mixed Versioning Variants + + [Fact] + public void FilterPackagesToDelete_WithMixedVersioningVariants_ShouldProcessBothTypes() + { + // Arrange + SetupMocks(); + var packages = new List + { + // Semantic versioning + new() + { + Id = 1, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) + }, + new() + { + Id = 2, Name = "test", Version = "1.1.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) + }, + // Date versioning + new() + { + Id = 3, Name = "test", Version = "2025.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) + }, + new() + { + Id = 4, Name = "test", Version = "2025.2.10.1-dev", Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-5) + } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert + Assert.NotNull(result); + // Prod releases should not be in delete list + Assert.DoesNotContain(result, p => p.Id == 1 || p.Id == 3); + } + + #endregion +} \ No newline at end of file diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceSemanticVersionTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceSemanticVersionTests.cs new file mode 100644 index 0000000..0bb03da --- /dev/null +++ b/sh.actions.package-cleanup.Tests/Service/PackageServiceSemanticVersionTests.cs @@ -0,0 +1,498 @@ +using Microsoft.Extensions.Configuration; +using Moq; +using sh.actions.package_cleanup.Models; +using sh.actions.package_cleanup.Service; +using Xunit; + +namespace sh.actions.package_cleanup.Tests.Service; + +public class PackageServiceSemanticVersionTests +{ + private Mock _configurationMock; + private Mock _giteaPackageServiceMock; + private IPackageService _packageService; + + private void SetupMocks() + { + _configurationMock = new Mock(); + _giteaPackageServiceMock = new Mock(); + _packageService = new PackageService(_configurationMock.Object, _giteaPackageServiceMock.Object); + } + + #region Version Detection Tests + + [Theory] + [InlineData("1.0.0.0")] + [InlineData("2.5.3.1")] + [InlineData("10.20.30.100")] + [InlineData("1.0.0")] + [InlineData("2.5")] + public void FilterPackagesToDelete_WithSemanticVersions_ShouldIdentifyCorrectly(string version) + { + // Arrange + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Single prod version should not be deleted (CLEAN_PROD_VERSIONS=false by default) + Assert.NotNull(result); + Assert.Empty(result); + } + + [Theory] + [InlineData("1.0.0.0-dev")] + [InlineData("2.5.3.1-alpha")] + [InlineData("10.20.30.100-beta")] + [InlineData("1.0.0-rc1")] + [InlineData("2.5-preview")] + public void FilterPackagesToDelete_WithSemanticVersionsAndSuffixes_ShouldIdentifyCorrectly(string version) + { + // Arrange + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Single prerelease/dev should be kept + Assert.NotNull(result); + Assert.Empty(result); + } + + #endregion + + #region Prod Release Tests - Semantic Versioning + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithOnlyProdReleases_ShouldReturnEmpty() + { + // Arrange - CLEAN_PROD_VERSIONS not set, defaults to false + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = "1.0.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, + new() { Id = 2, Name = "test", Version = "1.1.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, + new() { Id = 3, Name = "test", Version = "1.2.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Prod releases should be protected by default (CLEAN_PROD_VERSIONS=false) + Assert.Empty(result); + Assert.DoesNotContain(result, p => p.Id == 1 || p.Id == 2 || p.Id == 3); + } + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithProdReleasesAndCleaningEnabled_ShouldDeleteOldest() + { + // Arrange + SetupMocks(); + _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("true"); + _configurationMock.Setup(c => c["KEEP_PROD_VERSIONS"]).Returns("2"); + + var packages = new List + { + new() { Id = 1, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, + new() { Id = 2, Name = "test", Version = "1.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, + new() { Id = 3, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, + new() { Id = 4, Name = "test", Version = "2.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should delete oldest 2 (1.0.0.0 and 1.1.0.0), keep newest 2 (2.0.0.0 and 2.1.0.0) + Assert.Equal(2, result.Count); + Assert.Contains(result, p => p.Id == 1); + Assert.Contains(result, p => p.Id == 2); + Assert.DoesNotContain(result, p => p.Id == 3 || p.Id == 4); + } + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithMultipleProdVersions_ShouldKeepNewest() + { + // Arrange + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, + new() { Id = 2, Name = "test", Version = "1.0.1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, + new() { Id = 3, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, + new() { Id = 4, Name = "test", Version = "2.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Default is CLEAN_PROD_VERSIONS=false, so nothing should be deleted + Assert.Empty(result); + } + + #endregion + + #region Prerelease Tests - Semantic Versioning + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithPrereleaseVersions_ShouldKeepLatest() + { + // Arrange - Default KEEP_PRERELEASE_VERSIONS=3 + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = "1.0.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, + new() { Id = 2, Name = "test", Version = "1.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, + new() { Id = 3, Name = "test", Version = "1.0.0-rc1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) }, + new() { Id = 4, Name = "test", Version = "1.0.0-preview", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-2) } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should delete oldest 1 (alpha, id=1), keep newest 3 (beta, rc1, preview) + Assert.Single(result); + Assert.Contains(result, p => p.Id == 1); // alpha is oldest, should be deleted + Assert.DoesNotContain(result, p => p.Id == 2 || p.Id == 3 || p.Id == 4); // Keep newest 3 + } + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithManyPrereleases_ShouldEnforceLimit() + { + // Arrange - Custom KEEP_PRERELEASE_VERSIONS=2 + SetupMocks(); + _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); + + var packages = new List(); + for (int i = 0; i < 10; i++) + { + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"1.0.0-beta{i}", + Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-(10 - i)) + }); + } + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should delete 8 oldest, keep only 2 newest (Id=8,9) + Assert.Equal(8, result.Count); + Assert.DoesNotContain(result, p => p.Id >= 8); + Assert.Contains(result, p => p.Id < 8); + } + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithMixedPrereleases_ShouldOrderByVersion() + { + // Arrange + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = "1.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, + new() { Id = 2, Name = "test", Version = "2.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, + new() { Id = 3, Name = "test", Version = "1.5.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Default keep 3, so all should be kept + Assert.Empty(result); + } + + #endregion + + #region Dev Release Tests - Semantic Versioning + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithDevVersions_ShouldKeepLatest() + { + // Arrange - Default KEEP_DEV_VERSIONS=5 + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = "1.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, + new() { Id = 2, Name = "test", Version = "1.0.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, + new() { Id = 3, Name = "test", Version = "2.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Default keep 5, so all 3 should be kept + Assert.Empty(result); + } + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithManyDevReleases_ShouldEnforceLimit() + { + // Arrange - Custom KEEP_DEV_VERSIONS=3 + SetupMocks(); + _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("3"); + + var packages = new List(); + for (int i = 0; i < 10; i++) + { + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"1.0.{i}-dev", + Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-i) + }); + } + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should delete 7 oldest, keep only 3 newest (Id=7,8,9 are newest by CreatedAt) + Assert.Equal(7, result.Count); + Assert.DoesNotContain(result, p => p.Id >= 7); // Keep newest 3 + Assert.Contains(result, p => p.Id < 7); // Delete oldest 7 + } + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithManyDevReleases_ShouldIdentifyAll() + { + // Arrange + SetupMocks(); + var packages = new List(); + for (int i = 0; i < 20; i++) + { + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"1.0.{i}-dev", + Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-i) + }); + } + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Default keep 5, so 15 should be deleted + Assert.Equal(15, result.Count); + Assert.DoesNotContain(result, p => p.Id >= 15); // Keep newest 5 (Id 15-19) + Assert.Contains(result, p => p.Id < 15); + } + + #endregion + + #region Mixed Release Types - Semantic Versioning + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithMixedReleaseTypes_ShouldSeparateAndCleanCorrectly() + { + // Arrange - CLEAN_PROD_VERSIONS=false, KEEP_PRERELEASE_VERSIONS=2, KEEP_DEV_VERSIONS=2 + SetupMocks(); + _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); + _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("2"); + + var packages = new List + { + // Prod releases (should not be deleted - CLEAN_PROD_VERSIONS=false) + new() { Id = 1, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, + new() { Id = 2, Name = "test", Version = "1.1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-25) }, + // Prerelease (keep 2 newest) + new() { Id = 3, Name = "test", Version = "1.2.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, + new() { Id = 4, Name = "test", Version = "1.2.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-15) }, + new() { Id = 5, Name = "test", Version = "1.2.1-rc", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, + // Dev (keep 2 newest) + new() { Id = 6, Name = "test", Version = "1.2.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, + new() { Id = 7, Name = "test", Version = "1.2.2-dev", Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert + Assert.Equal(1, result.Count); // Should delete 1 prerelease (alpha), 0 dev (keep both) + Assert.DoesNotContain(result, p => p.Id <= 2); // Prod not deleted + Assert.DoesNotContain(result, p => p.Id >= 6); // Keep 2 latest dev + } + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithLargeSet_ShouldHandleCorrectly() + { + // Arrange + SetupMocks(); + _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("false"); + _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("3"); + _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("5"); + + var packages = new List(); + + // Add prod releases + for (int i = 0; i < 5; i++) + { + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"1.{i}.0", + Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-(5 - i)) + }); + } + + // Add prerelease + for (int i = 5; i < 15; i++) + { + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"2.0.{i - 5}-beta", + Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-(15 - i)) + }); + } + + // Add dev + for (int i = 15; i < 30; i++) + { + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"2.1.{i - 15}-dev", + Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-(30 - i)) + }); + } + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert + Assert.NotNull(result); + // Prod releases should never be deleted + Assert.DoesNotContain(result, p => p.Id < 5); + // Should delete 7 oldest prerelease (keep 3 newest) + var prereleaseDeleted = result.Where(p => p.Id >= 5 && p.Id < 15).ToList(); + Assert.Equal(7, prereleaseDeleted.Count); + // Should delete 10 oldest dev (keep 5 newest) + var devDeleted = result.Where(p => p.Id >= 15).ToList(); + Assert.Equal(10, devDeleted.Count); + } + + #endregion + + #region Edge Cases - Semantic Versioning + + [Theory] + [InlineData("0.0.0.0")] + [InlineData("99.99.99.99")] + [InlineData("100.0.0.0")] // Boundary case - might be detected as date version + public void FilterPackagesToDeleteBySemanticVersion_WithBoundaryVersions_ShouldHandleCorrectly(string version) + { + // Arrange + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should not throw, and boundary versions should be handled properly + Assert.NotNull(result); + // Versions 0.0.0.0 and 99.99.99.99 are semantic, 100.0.0.0 is likely treated as date (will fail date validation) + } + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithVersionsHavingDifferentLengths_ShouldHandleCorrectly() + { + // Arrange + SetupMocks(); + var packages = new List + { + new() { Id = 1, Name = "test", Version = "1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, + new() { Id = 2, Name = "test", Version = "1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, + new() { Id = 3, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) }, + new() { Id = 4, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-2) } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should not throw and should not delete any prod versions + Assert.NotNull(result); + // CLEAN_PROD_VERSIONS=false by default, so all versions should be kept + // Id=1 is invalid (only 1 component), ids 2-4 are valid but prod + var validVersions = new[] { 2, 3, 4 }; + foreach (var id in validVersions) + { + Assert.DoesNotContain(result, p => p.Id == id); + } + } + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithZeroKeepCount_ShouldKeepAll() + { + // Arrange - KEEP_DEV_VERSIONS=0 means keep all + SetupMocks(); + _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("0"); + + var packages = new List(); + for (int i = 0; i < 10; i++) + { + packages.Add(new GiteaPackage + { + Id = i, + Name = "test", + Version = $"1.0.{i}-dev", + Type = "docker", + CreatedAt = DateTime.UtcNow.AddDays(-i) + }); + } + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should keep all (0 means infinite/keep all) + Assert.Empty(result); + Assert.DoesNotContain(result, p => p.Id >= 0); // No packages should be deleted + } + + [Fact] + public void FilterPackagesToDeleteBySemanticVersion_WithInvalidConfigValues_ShouldUseDefaults() + { + // Arrange - Set invalid config values + SetupMocks(); + _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("invalid"); + _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("maybe"); + + var packages = new List + { + new() { Id = 1, Name = "test", Version = "1.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, + new() { Id = 2, Name = "test", Version = "1.0.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow }, + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should use defaults (KEEP_DEV_VERSIONS=5, CLEAN_PROD_VERSIONS=false) + // With 2 packages and keep 5, no deletion should happen + Assert.Empty(result); + Assert.DoesNotContain(result, p => p.Id == 1 || p.Id == 2); + } + + #endregion +} + diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceVersionDetectionTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceVersionDetectionTests.cs new file mode 100644 index 0000000..f8eb400 --- /dev/null +++ b/sh.actions.package-cleanup.Tests/Service/PackageServiceVersionDetectionTests.cs @@ -0,0 +1,349 @@ +using Microsoft.Extensions.Configuration; +using Moq; +using sh.actions.package_cleanup.Models; +using sh.actions.package_cleanup.Service; +using Xunit; + +namespace sh.actions.package_cleanup.Tests.Service; + +public class PackageServiceVersionDetectionTests +{ + private readonly IPackageService _packageService; + private readonly Mock _configurationMock; + private readonly Mock _giteaPackageServiceMock; + + public PackageServiceVersionDetectionTests() + { + _configurationMock = new Mock(); + _giteaPackageServiceMock = new Mock(); + _packageService = new PackageService(_configurationMock.Object, _giteaPackageServiceMock.Object); + } + + #region Release Type Detection Tests + + [Theory] + [InlineData("1.0.0")] + [InlineData("2.5.3")] + [InlineData("10.20.30")] + [InlineData("2025.2.14.1")] + public void FilterPackagesToDelete_WithProdReleases_ShouldIdentifyAsNonDeletable(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Prod releases should not be in delete list (CLEAN_PROD_VERSIONS=false by default) + Assert.DoesNotContain(result, p => p.Id == 1); + Assert.Empty(result); // No packages should be deleted without CLEAN_PROD_VERSIONS=true + } + + [Theory] + [InlineData("1.0.0-alpha")] + [InlineData("1.0.0-ALPHA")] + [InlineData("2.5.3-alpha123")] + [InlineData("1.0.0-beta")] + [InlineData("1.0.0-BETA")] + [InlineData("1.0.0-rc")] + [InlineData("1.0.0-RC1")] + [InlineData("1.0.0-preview")] + [InlineData("1.0.0-PREVIEW")] + public void FilterPackagesToDelete_WithPrereleaseVersions_ShouldIdentifyCorrectly(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Prerelease should be kept (default KEEP_PRERELEASE_VERSIONS=3, so 1 package is kept) + Assert.NotNull(result); + Assert.Empty(result); // Single prerelease should be kept as it's within the limit + } + + [Theory] + [InlineData("1.0.0-dev")] + [InlineData("1.0.0-DEV")] + [InlineData("2.5.3-dev123")] + [InlineData("1.0.0-development")] + [InlineData("1.0.0-DEV")] + public void FilterPackagesToDelete_WithDevVersions_ShouldIdentifyCorrectly(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Dev version should be kept (default KEEP_DEV_VERSIONS=5, so 1 package is kept) + Assert.NotNull(result); + Assert.Empty(result); // Single dev version should be kept as it's within the limit + } + + #endregion + + #region Semantic Version Detection Tests + + [Theory] + [InlineData("1.0")] + [InlineData("1.0.0")] + [InlineData("1.0.0.0")] + [InlineData("2.5")] + [InlineData("2.5.3")] + [InlineData("2.5.3.100")] + [InlineData("0.0.0.0")] + [InlineData("99.99.99.99")] + public void FilterPackagesToDelete_WithValidSemanticVersions_ShouldProcess(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should process without throwing and not delete prod releases + Assert.NotNull(result); + Assert.DoesNotContain(result, p => p.Id == 1); // Prod version should not be deleted + } + + [Theory] + [InlineData("1")] // Only 1 component + [InlineData("1.0.0.0.0")] // 5 components + [InlineData("1.a.0.0")] // Non-numeric + [InlineData("a.b.c.d")] // All non-numeric + public void FilterPackagesToDelete_WithInvalidSemanticVersions_ShouldSkip(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow }, + new() { Id = 2, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Invalid version should be skipped, only valid semantic version (id=2) is processed + Assert.NotNull(result); + // Should not crash and should only consider the valid version + Assert.DoesNotContain(result, p => p.Id == 2); // Valid prod should not be deleted + } + + [Theory] + [InlineData("101.0.0.0")] + [InlineData("200.5.3.100")] + [InlineData("1000.0.0.0")] + public void FilterPackagesToDelete_WithLargeMajorVersion_ShouldTreatAsDateVersion(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Large major versions (>100) are detected as date versions, not semantic + // These will fail date validation (invalid month/day) and be skipped + Assert.NotNull(result); + } + + #endregion + + #region Date Version Detection Tests + + [Theory] + [InlineData("2024.1.1.1")] + [InlineData("2025.2.14.1")] + [InlineData("2026.12.31.999")] + [InlineData("2000.6.15.100")] + [InlineData("1999.1.1.1")] + public void FilterPackagesToDelete_WithValidDateVersions_ShouldProcess(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should process without throwing and not delete prod date versions + Assert.NotNull(result); + Assert.DoesNotContain(result, p => p.Id == 1); // Prod date version should not be deleted + } + + [Theory] + [InlineData("2025.1.1")] // 3 components (missing build) + [InlineData("2025.1.1.1.1")] // 5 components + [InlineData("2025.13.1.1")] // Month out of range + [InlineData("2025.0.1.1")] // Month zero + [InlineData("2025.2.32.1")] // Day out of range + [InlineData("2025.2.0.1")] // Day zero + [InlineData("999.2.14.1")] // Year too small + [InlineData("3000.2.14.1")] // Year too large + [InlineData("2025.a.14.1")] // Non-numeric month + public void FilterPackagesToDelete_WithInvalidDateVersions_ShouldSkip(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow }, + new() { Id = 2, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Invalid date version should be skipped, valid semantic version (id=2) is processed as prod + Assert.NotNull(result); + Assert.DoesNotContain(result, p => p.Id == 2); // Valid semantic prod should not be deleted + } + + [Theory] + [InlineData("1000.1.1.1")] // Minimum valid year + [InlineData("2999.12.31.999")] // Maximum valid year + [InlineData("2025.1.1.1")] // January 1 + [InlineData("2025.12.31.1")] // December 31 + public void FilterPackagesToDelete_WithDateVersionBoundaries_ShouldProcess(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Boundary date versions should be recognized as valid + Assert.NotNull(result); + Assert.DoesNotContain(result, p => p.Id == 1); // Prod date version should not be deleted + } + + #endregion + + #region Case Sensitivity Tests + + [Theory] + [InlineData("1.0.0-alpha")] + [InlineData("1.0.0-ALPHA")] + [InlineData("1.0.0-Alpha")] + [InlineData("1.0.0-AlPhA")] + public void FilterPackagesToDelete_WithDifferentCasePrerelease_ShouldHandleCorrectly(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - All case variations should be recognized as prerelease + Assert.NotNull(result); + Assert.Empty(result); // Single prerelease should be kept + } + + [Theory] + [InlineData("1.0.0-dev")] + [InlineData("1.0.0-DEV")] + [InlineData("1.0.0-Dev")] + [InlineData("1.0.0-DeV")] + public void FilterPackagesToDelete_WithDifferentCaseDev_ShouldHandleCorrectly(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - All case variations should be recognized as dev version + Assert.NotNull(result); + Assert.Empty(result); // Single dev version should be kept + } + + #endregion + + #region Suffix/Identifier Tests + + [Theory] + [InlineData("1.0.0-alpha1")] + [InlineData("1.0.0-alpha.1")] + [InlineData("1.0.0-beta2")] + [InlineData("1.0.0-rc.1")] + [InlineData("2025.2.14.1-dev.1")] + [InlineData("2025.2.14.1-dev123")] + public void FilterPackagesToDelete_WithCompoundSuffixes_ShouldIdentifyCorrectly(string version) + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Compound suffixes should be recognized and not cause errors + Assert.NotNull(result); + Assert.Empty(result); // Single prerelease/dev should be kept + } + + #endregion + + #region Empty and Null Tests + + [Fact] + public void FilterPackagesToDelete_WithEmptyList_ShouldReturnEmpty() + { + // Arrange + var packages = new List(); + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Empty input should return empty result + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void FilterPackagesToDelete_WithNullableInputs_ShouldHandleGracefully() + { + // Arrange + var packages = new List + { + new() { Id = 1, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } + }; + + // Act + var result = _packageService.FilterPackagesToDelete(packages); + + // Assert - Should handle without throwing and not delete prod + Assert.NotNull(result); + Assert.Empty(result); // Prod should not be deleted + } + + #endregion +} + diff --git a/sh.actions.package-cleanup.Tests/sh.actions.package-cleanup.Tests.csproj b/sh.actions.package-cleanup.Tests/sh.actions.package-cleanup.Tests.csproj new file mode 100644 index 0000000..717112f --- /dev/null +++ b/sh.actions.package-cleanup.Tests/sh.actions.package-cleanup.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + true + enable + enable + sh.actions.package_cleanup.Tests + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/sh.actions.package-cleanup/ConfigurationExtension.cs b/sh.actions.package-cleanup/ConfigurationExtension.cs new file mode 100644 index 0000000..d6ca7ca --- /dev/null +++ b/sh.actions.package-cleanup/ConfigurationExtension.cs @@ -0,0 +1,26 @@ +namespace sh.actions.package_cleanup; + +public static class ConfigurationExtension +{ + private static void EnsureVariable(IConfiguration configuration, string key) + { + var value = configuration[key]; + if (string.IsNullOrEmpty(value)) + { + throw new InvalidOperationException($"Configuration variable '{key}' is required but was not found."); + } + } + + public static IConfigurationBuilder EnsureGiteaConfig(this IConfigurationBuilder builder) + { + var configuration = builder.Build(); + + EnsureVariable(configuration, "URL"); + EnsureVariable(configuration, "OWNER"); + EnsureVariable(configuration, "TYPE"); + EnsureVariable(configuration, "NAME"); + EnsureVariable(configuration, "API_TOKEN"); + + return builder; + } +} \ No newline at end of file diff --git a/sh.actions/sh.actions.package-cleanup/Models/GiteaPackage.cs b/sh.actions.package-cleanup/Models/GiteaPackage.cs similarity index 100% rename from sh.actions/sh.actions.package-cleanup/Models/GiteaPackage.cs rename to sh.actions.package-cleanup/Models/GiteaPackage.cs diff --git a/sh.actions.package-cleanup/Program.cs b/sh.actions.package-cleanup/Program.cs new file mode 100644 index 0000000..be417e2 --- /dev/null +++ b/sh.actions.package-cleanup/Program.cs @@ -0,0 +1,21 @@ +using sh.actions.package_cleanup; +using sh.actions.package_cleanup.Service; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Configuration + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .EnsureGiteaConfig(); + +builder.Services + .AddSingleton() + .AddScoped() + .AddHostedService() + .AddHttpClient(); + + +var host = builder.Build(); + +host.Run(); \ No newline at end of file diff --git a/sh.actions.package-cleanup/Properties/launchSettings.json b/sh.actions.package-cleanup/Properties/launchSettings.json new file mode 100644 index 0000000..f4179fd --- /dev/null +++ b/sh.actions.package-cleanup/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "sh.actions.package_cleanup": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/sh.actions.package-cleanup/Service/GiteaPackageService.cs b/sh.actions.package-cleanup/Service/GiteaPackageService.cs new file mode 100644 index 0000000..1efcff4 --- /dev/null +++ b/sh.actions.package-cleanup/Service/GiteaPackageService.cs @@ -0,0 +1,95 @@ +namespace sh.actions.package_cleanup.Service; + +using System.Net.Http.Json; +using Microsoft.Extensions.Logging; +using sh.actions.package_cleanup.Models; + +public class GiteaPackageService( + ILogger logger, + IConfiguration configuration, + HttpClient httpClient) + : IGiteaPackageService +{ + public async Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default) + { + try + { + var baseUrl = + $"{configuration["URL"]?.TrimEnd('/')}/packages/{configuration["OWNER"]}/{configuration["TYPE"]}/{configuration["NAME"]}"; + var allPackages = new List(); + var page = 1; + + while (true) + { + var url = $"{baseUrl}?page={page}"; + + logger.LogInformation("Fetching packages from Gitea: {Url}", url); + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Authorization", $"token {configuration["API_TOKEN"]}"); + + var response = await httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + + var packages = + await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken) + ?? []; + + if (packages.Count == 0) + { + break; + } + + allPackages.AddRange(packages); + logger.LogInformation("Fetched {Count} packages from page {Page}", packages.Count, page); + + page++; + } + + logger.LogInformation("Successfully fetched {Count} packages in total", allPackages.Count); + + return allPackages; + } + catch (HttpRequestException ex) + { + logger.LogError(ex, "Error fetching packages from Gitea"); + throw; + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected error fetching packages"); + throw; + } + } + + public async Task DeletePackage(GiteaPackage package, CancellationToken cancellationToken = default) + { + try + { + var url = + $"{configuration["URL"]?.TrimEnd('/')}/packages/{configuration["OWNER"]}/{package.Type}/{package.Name}/{package.Version}"; + logger.LogInformation("Deleting package {PackageName} (ID: {PackageId}) from Gitea: {Url}", package.Name, + package.Id, url); + + var request = new HttpRequestMessage(HttpMethod.Delete, url); + request.Headers.Add("Authorization", $"token {configuration["API_TOKEN"]}"); + + var response = await httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + + logger.LogInformation("Successfully deleted package {PackageName} (ID: {PackageId})", package.Name, + package.Id); + } + catch (HttpRequestException ex) + { + logger.LogError(ex, "Error deleting package {PackageName} (ID: {PackageId}) from Gitea", package.Name, + package.Id); + throw; + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected error deleting package {PackageName} (ID: {PackageId})", package.Name, + package.Id); + throw; + } + } +} \ No newline at end of file diff --git a/sh.actions/sh.actions.package-cleanup/Service/IGiteaPackageService.cs b/sh.actions.package-cleanup/Service/IGiteaPackageService.cs similarity index 71% rename from sh.actions/sh.actions.package-cleanup/Service/IGiteaPackageService.cs rename to sh.actions.package-cleanup/Service/IGiteaPackageService.cs index bb3017e..7df007e 100644 --- a/sh.actions/sh.actions.package-cleanup/Service/IGiteaPackageService.cs +++ b/sh.actions.package-cleanup/Service/IGiteaPackageService.cs @@ -5,4 +5,5 @@ using sh.actions.package_cleanup.Models; public interface IGiteaPackageService { Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default); + Task DeletePackage(GiteaPackage package, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/sh.actions.package-cleanup/Service/IPackageService.cs b/sh.actions.package-cleanup/Service/IPackageService.cs new file mode 100644 index 0000000..453c881 --- /dev/null +++ b/sh.actions.package-cleanup/Service/IPackageService.cs @@ -0,0 +1,9 @@ +using sh.actions.package_cleanup.Models; + +namespace sh.actions.package_cleanup.Service; + +public interface IPackageService +{ + Task> GetFilteredPackages(); + List FilterPackagesToDelete(List packages); +} \ No newline at end of file diff --git a/sh.actions.package-cleanup/Service/PackageService.cs b/sh.actions.package-cleanup/Service/PackageService.cs new file mode 100644 index 0000000..00df577 --- /dev/null +++ b/sh.actions.package-cleanup/Service/PackageService.cs @@ -0,0 +1,311 @@ +using sh.actions.package_cleanup.Models; + +namespace sh.actions.package_cleanup.Service; + +public class PackageService(IConfiguration configuration, IGiteaPackageService packageService) : IPackageService +{ + public async Task> GetFilteredPackages() + { + var packages = (await packageService.GetPackagesByOwnerAsync()).ToList(); + return packages; + } + + public List FilterPackagesToDelete(List packages) + { + var packagesToDelete = new List(); + + // Group packages by versioning variant + var semanticVersionPackages = packages.Where(p => IsSemanticVersion(p.Version)).ToList(); + var dateVersionPackages = packages.Where(p => IsDateVersion(p.Version)).ToList(); + + // Process each group with their respective variant logic + packagesToDelete.AddRange(FilterPackagesToDeleteBySemanticVersion(semanticVersionPackages)); + packagesToDelete.AddRange(FilterPackagesToDeleteByDateVersion(dateVersionPackages)); + + return packagesToDelete; + } + + private List FilterPackagesToDeleteBySemanticVersion(List packages) + { + var packagesToDelete = new List(); + + // Group packages by release type + // Note: Dev releases must be checked before prerelease since -dev is a suffix + var devPackages = packages.Where(p => IsDevRelease(p.Version)) + .OrderByDescending(p => ParseSemanticVersion(p.Version)) + .ThenByDescending(p => p.CreatedAt) + .ToList(); + var prereleasePackages = packages.Where(p => IsPrereleaseRelease(p.Version) && !IsDevRelease(p.Version)) + .OrderByDescending(p => ParseSemanticVersion(p.Version)) + .ThenByDescending(p => p.CreatedAt) + .ToList(); + var prodPackages = packages.Where(p => IsProdRelease(p.Version)) + .OrderByDescending(p => ParseSemanticVersion(p.Version)) + .ThenByDescending(p => p.CreatedAt) + .ToList(); + + // Apply cleanup rules for each type + packagesToDelete.AddRange(FilterProdReleases(prodPackages)); + packagesToDelete.AddRange(FilterPrereleaseReleases(prereleasePackages)); + packagesToDelete.AddRange(FilterDevReleases(devPackages)); + + return packagesToDelete; + } + + private List FilterPackagesToDeleteByDateVersion(List packages) + { + var packagesToDelete = new List(); + + // Group packages by release type + // Note: Dev releases must be checked before prerelease since -dev is a suffix + var devPackages = packages.Where(p => IsDevRelease(p.Version)) + .OrderByDescending(p => ParseDateVersion(p.Version)) + .ThenByDescending(p => p.CreatedAt) + .ToList(); + var prereleasePackages = packages.Where(p => IsPrereleaseRelease(p.Version) && !IsDevRelease(p.Version)) + .OrderByDescending(p => ParseDateVersion(p.Version)) + .ThenByDescending(p => p.CreatedAt) + .ToList(); + var prodPackages = packages.Where(p => IsProdRelease(p.Version)) + .OrderByDescending(p => ParseDateVersion(p.Version)) + .ThenByDescending(p => p.CreatedAt) + .ToList(); + + // Apply cleanup rules for each type + packagesToDelete.AddRange(FilterProdReleasesDateVersion(prodPackages)); + packagesToDelete.AddRange(FilterPrereleaseReleasesDateVersion(prereleasePackages)); + packagesToDelete.AddRange(FilterDevReleasesDateVersion(devPackages)); + + return packagesToDelete; + } + + // Prod release methods for semantic versioning + private List FilterProdReleases(List packages) + { + // Check if prod cleaning is enabled + if (!GetBoolConfigValue("CLEAN_PROD_VERSIONS", false)) + return new List(); + + // Get keep count + var keepCount = GetIntConfigValue("KEEP_PROD_VERSIONS", 5); + + // If keepCount <= 0, keep all + if (keepCount <= 0) + return new List(); + + // Packages are already sorted descending by version + // Keep top N, delete rest + return packages.Skip(keepCount).ToList(); + } + + // Prerelease methods for semantic versioning + private List FilterPrereleaseReleases(List packages) + { + // Get keep count + var keepCount = GetIntConfigValue("KEEP_PRERELEASE_VERSIONS", 3); + + // If keepCount <= 0, keep all + if (keepCount <= 0) + return new List(); + + // Packages are already sorted descending by version + // Keep top N, delete rest + return packages.Skip(keepCount).ToList(); + } + + // Dev release methods for semantic versioning + private List FilterDevReleases(List packages) + { + // Get keep count + var keepCount = GetIntConfigValue("KEEP_DEV_VERSIONS", 5); + + // If keepCount <= 0, keep all + if (keepCount <= 0) + return new List(); + + // Packages are already sorted descending by version + // Keep top N, delete rest + return packages.Skip(keepCount).ToList(); + } + + // Prod release methods for date-based versioning + private List FilterProdReleasesDateVersion(List packages) + { + // Check if prod cleaning is enabled + if (!GetBoolConfigValue("CLEAN_PROD_VERSIONS", false)) + return new List(); + + // Get keep count + var keepCount = GetIntConfigValue("KEEP_PROD_VERSIONS", 5); + + // If keepCount <= 0, keep all + if (keepCount <= 0) + return new List(); + + // Packages are already sorted descending by version + // Keep top N, delete rest + return packages.Skip(keepCount).ToList(); + } + + // Prerelease methods for date-based versioning + private List FilterPrereleaseReleasesDateVersion(List packages) + { + // Get keep count + var keepCount = GetIntConfigValue("KEEP_PRERELEASE_VERSIONS", 3); + + // If keepCount <= 0, keep all + if (keepCount <= 0) + return new List(); + + // Packages are already sorted descending by version + // Keep top N, delete rest + return packages.Skip(keepCount).ToList(); + } + + // Dev release methods for date-based versioning + private List FilterDevReleasesDateVersion(List packages) + { + // Get keep count + var keepCount = GetIntConfigValue("KEEP_DEV_VERSIONS", 5); + + // If keepCount <= 0, keep all + if (keepCount <= 0) + return new List(); + + // Packages are already sorted descending by version + // Keep top N, delete rest + return packages.Skip(keepCount).ToList(); + } + + // Helper methods to identify release types + private bool IsProdRelease(string version) + { + // Prod versions typically don't have pre-release identifiers (alpha, beta, rc, dev, etc.) + return !version.Contains("-", StringComparison.OrdinalIgnoreCase); + } + + private bool IsPrereleaseRelease(string version) + { + // Prerelease versions contain identifiers like -alpha, -beta, -rc, -preview + // But NOT -dev, which is a separate release type + var lowerVersion = version.ToLowerInvariant(); + return (lowerVersion.Contains("-alpha") || lowerVersion.Contains("-beta") || + lowerVersion.Contains("-rc") || lowerVersion.Contains("-preview")) && !lowerVersion.Contains("-dev"); + } + + private bool IsDevRelease(string version) + { + // Dev versions contain identifiers like -dev or similar + return version.ToLowerInvariant().Contains("-dev"); + } + + // Helper methods to identify versioning variant + private bool IsSemanticVersion(string version) + { + // Semantic versioning: major.minor.micro.build with typically smaller numbers + // Examples: 1.2.3.4, 2.0.1.100 + var versionPart = version.Split('-')[0]; + var parts = versionPart.Split('.').Where(p => !string.IsNullOrEmpty(p)).ToArray(); + + if (parts.Length < 2 || parts.Length > 4) + return false; + + // Try to parse all parts as numbers + if (!parts.All(p => int.TryParse(p, out var num))) + return false; + + // Semantic versioning typically has smaller major version numbers (0-20 range) + if (int.TryParse(parts[0], out var major) && major > 100) + return false; // Likely a date version if first number > 100 + + return true; + } + + private bool IsDateVersion(string version) + { + // Date-based versioning: year.month.day.build + // Examples: 2024.12.25.1, 2025.2.14.100 + var versionPart = version.Split('-')[0]; + var parts = versionPart.Split('.').Where(p => !string.IsNullOrEmpty(p)).ToArray(); + + if (parts.Length != 4) + return false; + + // Try to parse all parts as numbers + if (!parts.All(p => int.TryParse(p, out var num))) + return false; + + // Date version should have year > 1000 and < 3000 + if (!int.TryParse(parts[0], out var year) || year < 1000 || year > 3000) + return false; + + // Month should be 1-12 + if (!int.TryParse(parts[1], out var month) || month < 1 || month > 12) + return false; + + // Day should be 1-31 + if (!int.TryParse(parts[2], out var day) || day < 1 || day > 31) + return false; + + return true; + } + + // Version parsing methods + private (int major, int minor, int micro, int build) ParseSemanticVersion(string version) + { + // Remove suffix if present (e.g., "-alpha", "-dev") + var versionPart = version.Split('-')[0]; + var parts = versionPart.Split('.').Select(p => int.TryParse(p, out var num) ? num : 0).ToArray(); + + return ( + parts.Length > 0 ? parts[0] : 0, + parts.Length > 1 ? parts[1] : 0, + parts.Length > 2 ? parts[2] : 0, + parts.Length > 3 ? parts[3] : 0 + ); + } + + private (int year, int month, int day, int build) ParseDateVersion(string version) + { + // Remove suffix if present (e.g., "-alpha", "-dev") + var versionPart = version.Split('-')[0]; + var parts = versionPart.Split('.').Select(p => int.TryParse(p, out var num) ? num : 0).ToArray(); + + return ( + parts.Length > 0 ? parts[0] : 0, + parts.Length > 1 ? parts[1] : 0, + parts.Length > 2 ? parts[2] : 0, + parts.Length > 3 ? parts[3] : 0 + ); + } + + // Configuration helper methods + private int GetIntConfigValue(string key, int defaultValue) + { + var value = configuration[key]; + if (string.IsNullOrEmpty(value)) + return defaultValue; + + if (int.TryParse(value, out var result)) + return result; + + return defaultValue; + } + + private bool GetBoolConfigValue(string key, bool defaultValue) + { + var value = configuration[key]; + if (string.IsNullOrEmpty(value)) + return defaultValue; + + if (bool.TryParse(value, out var result)) + return result; + + // Handle common string representations + if (value.Equals("true", StringComparison.OrdinalIgnoreCase) || + value.Equals("1", StringComparison.OrdinalIgnoreCase)) + return true; + + return defaultValue; + } +} \ No newline at end of file diff --git a/sh.actions.package-cleanup/Worker.cs b/sh.actions.package-cleanup/Worker.cs new file mode 100644 index 0000000..d4d857f --- /dev/null +++ b/sh.actions.package-cleanup/Worker.cs @@ -0,0 +1,41 @@ +using sh.actions.package_cleanup.Models; +using sh.actions.package_cleanup.Service; + +namespace sh.actions.package_cleanup; + +public class Worker( + ILogger logger, + IConfiguration configuration, + IHostApplicationLifetime appLifetime, + IPackageService packageService, + IGiteaPackageService giteaPackageService +) : BackgroundService +{ + private async Task DeletePackages(List packages, CancellationToken cancellationToken = default) + { + if (configuration["DRY_RUN"]?.ToLower() == "true") + { + logger.LogInformation("Dry run enabled, not deleting packages"); + return; + } + + foreach (var giteaPackage in packages) + { + await giteaPackageService.DeletePackage(giteaPackage, cancellationToken); + } + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + var packages = await packageService.GetFilteredPackages(); + Console.WriteLine($"Found {packages.Count()} packages"); + + var packagesToDelete = packageService.FilterPackagesToDelete(packages); + Console.WriteLine($"Found {packagesToDelete.Count()} packages to delete"); + + await DeletePackages(packagesToDelete, cancellationToken); + + logger.LogInformation("Cleanup finished, stopping application"); + appLifetime.StopApplication(); + } +} \ No newline at end of file diff --git a/sh.actions.package-cleanup/appsettings.json b/sh.actions.package-cleanup/appsettings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/sh.actions.package-cleanup/appsettings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj b/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj new file mode 100644 index 0000000..09f4696 --- /dev/null +++ b/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + enable + enable + dotnet-sh.actions.package_cleanup-2b7a013f-ec22-4325-9832-0c9ca9b8ced9 + sh.actions.package_cleanup + + + + + + + diff --git a/sh.actions.sln b/sh.actions.sln index 2e8a884..af1b34c 100644 --- a/sh.actions.sln +++ b/sh.actions.sln @@ -1,16 +1,44 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "sh.actions.package-cleanup", "sh.actions/sh.actions.package-cleanup\sh.actions.package-cleanup.csproj", "{F4995B6A-2CA1-4CD4-AEAC-BE397AF1910A}" +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "sh.actions.package-cleanup", "sh.actions.package-cleanup\sh.actions.package-cleanup.csproj", "{2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "sh.actions.package-cleanup.Tests", "sh.actions.package-cleanup.Tests\sh.actions.package-cleanup.Tests.csproj", "{8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F4995B6A-2CA1-4CD4-AEAC-BE397AF1910A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F4995B6A-2CA1-4CD4-AEAC-BE397AF1910A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F4995B6A-2CA1-4CD4-AEAC-BE397AF1910A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F4995B6A-2CA1-4CD4-AEAC-BE397AF1910A}.Release|Any CPU.Build.0 = Release|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Debug|x64.ActiveCfg = Debug|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Debug|x64.Build.0 = Debug|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Debug|x86.ActiveCfg = Debug|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Debug|x86.Build.0 = Debug|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Release|Any CPU.Build.0 = Release|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Release|x64.ActiveCfg = Release|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Release|x64.Build.0 = Release|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Release|x86.ActiveCfg = Release|Any CPU + {2C97D0E2-CD3F-4468-B963-E1F0211C4D3A}.Release|x86.Build.0 = Release|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Debug|x64.ActiveCfg = Debug|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Debug|x64.Build.0 = Debug|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Debug|x86.ActiveCfg = Debug|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Debug|x86.Build.0 = Debug|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Release|Any CPU.Build.0 = Release|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Release|x64.ActiveCfg = Release|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Release|x64.Build.0 = Release|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Release|x86.ActiveCfg = Release|Any CPU + {8F1A9B3E-5D2C-4A1F-B8E7-9C2D5F6A3B1C}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal diff --git a/sh.actions.sln.DotSettings.user b/sh.actions.sln.DotSettings.user new file mode 100644 index 0000000..eb46bbe --- /dev/null +++ b/sh.actions.sln.DotSettings.user @@ -0,0 +1,7 @@ + + ForceIncluded + ForceIncluded + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <Solution /> +</SessionState> + \ No newline at end of file diff --git a/sh.actions/sh.actions.package-cleanup/Models/GiteaConfig.cs b/sh.actions/sh.actions.package-cleanup/Models/GiteaConfig.cs deleted file mode 100644 index ae76675..0000000 --- a/sh.actions/sh.actions.package-cleanup/Models/GiteaConfig.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace sh.actions.package_cleanup.Models; - -/// -/// Konfiguration für Gitea-Verbindung -/// -public class GiteaConfig -{ - /// - /// Basis-URL der Gitea-Instanz (z.B. https://git.example.com) - /// - public string GiteaUrl { get; set; } = string.Empty; - - /// - /// API-Token für Authentifizierung - /// - public string ApiToken { get; set; } = string.Empty; - - /// - /// Owner/Username des Pakets - /// - public string Owner { get; set; } = string.Empty; -} - diff --git a/sh.actions/sh.actions.package-cleanup/Program.cs b/sh.actions/sh.actions.package-cleanup/Program.cs deleted file mode 100644 index 05d239c..0000000 --- a/sh.actions/sh.actions.package-cleanup/Program.cs +++ /dev/null @@ -1,65 +0,0 @@ -// See https://aka.ms/new-console-template for more information -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using sh.actions.package_cleanup.Models; -using sh.actions.package_cleanup.Service; - -Console.WriteLine("Hello, World!"); -// -// // Setup Dependency Injection -// var services = new ServiceCollection(); -// -// // Add Logging -// services.AddLogging(builder => -// builder.AddConsole() -// .SetMinimumLevel(LogLevel.Information) -// ); -// -// // Load configuration from environment variables or appsettings -// var giteaUrl = Environment.GetEnvironmentVariable("GITEA_URL") ?? "https://git.example.com"; -// var apiToken = Environment.GetEnvironmentVariable("GITEA_API_TOKEN") ?? string.Empty; -// var owner = Environment.GetEnvironmentVariable("GITEA_OWNER") ?? "sh-edraft"; -// -// var giteaConfig = new GiteaConfig -// { -// GiteaUrl = giteaUrl, -// ApiToken = apiToken, -// Owner = owner -// }; -// -// services.AddSingleton(giteaConfig); -// -// // Add HttpClient -// services.AddHttpClient(); -// -// // Build service provider -// var serviceProvider = services.BuildServiceProvider(); -// -// try -// { -// // Get the service and execute -// var packageService = serviceProvider.GetRequiredService(); -// var logger = serviceProvider.GetRequiredService>(); -// -// logger.LogInformation("Starting package cleanup tool"); -// logger.LogInformation("Gitea URL: {GiteaUrl}", giteaUrl); -// logger.LogInformation("Owner: {Owner}", owner); -// -// using var cts = new CancellationTokenSource(); -// var packages = await packageService.GetPackagesByOwnerAsync(cts.Token); -// -// logger.LogInformation("Retrieved {Count} packages", packages.Count()); -// -// foreach (var package in packages) -// { -// logger.LogInformation("Package: {Name} (Version: {Version})", package.Name, package.Version); -// } -// -// logger.LogInformation("Package cleanup tool completed successfully"); -// } -// catch (Exception ex) -// { -// var logger = serviceProvider.GetRequiredService>(); -// logger.LogError(ex, "Application error"); -// Environment.Exit(1); -// } diff --git a/sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs b/sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs deleted file mode 100644 index bd13abe..0000000 --- a/sh.actions/sh.actions.package-cleanup/Service/GiteaPackageService.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace sh.actions.package_cleanup.Service; - -using System.Net.Http.Json; -using Microsoft.Extensions.Logging; -using sh.actions.package_cleanup.Models; - -public class GiteaPackageService(HttpClient httpClient, GiteaConfig config, ILogger logger) - : IGiteaPackageService -{ - public async Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default) - { - try - { - var url = $"{config.GiteaUrl.TrimEnd('/')}/api/v1/packages/{config.Owner}"; - - logger.LogInformation("Fetching packages from Gitea: {Url}", url); - - var request = new HttpRequestMessage(HttpMethod.Get, url); - - // Add authentication if token is provided - if (!string.IsNullOrEmpty(config.ApiToken)) - { - request.Headers.Add("Authorization", $"token {config.ApiToken}"); - } - - var response = await httpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); - - var packages = - await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken) - ?? new List(); - - logger.LogInformation("Successfully fetched {Count} packages", packages.Count); - - return packages; - } - catch (HttpRequestException ex) - { - logger.LogError(ex, "Error fetching packages from Gitea"); - throw; - } - catch (Exception ex) - { - logger.LogError(ex, "Unexpected error fetching packages"); - throw; - } - } -} \ No newline at end of file diff --git a/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj b/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj deleted file mode 100644 index 038a930..0000000 --- a/sh.actions/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - Exe - net10.0 - sh.actions.package_cleanup - enable - enable - 0.0.1 - - - true - true - true - true - full - - - - - - - - - - - - - - - From 1764ddcb2ba900170c08c00b31d50ca89595a29a Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 14 Feb 2026 20:52:20 +0100 Subject: [PATCH 17/37] Smaller fix --- .../Service/PackageService.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sh.actions.package-cleanup/Service/PackageService.cs b/sh.actions.package-cleanup/Service/PackageService.cs index 00df577..23a37eb 100644 --- a/sh.actions.package-cleanup/Service/PackageService.cs +++ b/sh.actions.package-cleanup/Service/PackageService.cs @@ -189,14 +189,15 @@ public class PackageService(IConfiguration configuration, IGiteaPackageService p // Prerelease versions contain identifiers like -alpha, -beta, -rc, -preview // But NOT -dev, which is a separate release type var lowerVersion = version.ToLowerInvariant(); - return (lowerVersion.Contains("-alpha") || lowerVersion.Contains("-beta") || - lowerVersion.Contains("-rc") || lowerVersion.Contains("-preview")) && !lowerVersion.Contains("-dev"); + return (lowerVersion.Contains("alpha") || lowerVersion.Contains("beta") || + lowerVersion.Contains("rc") || lowerVersion.Contains("preview")) || + lowerVersion.Contains("staging") && !lowerVersion.Contains("dev"); } private bool IsDevRelease(string version) { - // Dev versions contain identifiers like -dev or similar - return version.ToLowerInvariant().Contains("-dev"); + // Dev versions contain identifiers like dev or similar + return version.ToLowerInvariant().Contains("dev"); } // Helper methods to identify versioning variant @@ -256,7 +257,7 @@ public class PackageService(IConfiguration configuration, IGiteaPackageService p // Remove suffix if present (e.g., "-alpha", "-dev") var versionPart = version.Split('-')[0]; var parts = versionPart.Split('.').Select(p => int.TryParse(p, out var num) ? num : 0).ToArray(); - + return ( parts.Length > 0 ? parts[0] : 0, parts.Length > 1 ? parts[1] : 0, @@ -270,7 +271,7 @@ public class PackageService(IConfiguration configuration, IGiteaPackageService p // Remove suffix if present (e.g., "-alpha", "-dev") var versionPart = version.Split('-')[0]; var parts = versionPart.Split('.').Select(p => int.TryParse(p, out var num) ? num : 0).ToArray(); - + return ( parts.Length > 0 ? parts[0] : 0, parts.Length > 1 ? parts[1] : 0, From 8586a09cd0fbd29181a09f837a6c2cde0aaca00f Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 02:58:44 +0100 Subject: [PATCH 18/37] Added package cleanup --- .../Service/PackageServiceDateVersionTests.cs | 617 ------------------ .../PackageServiceSemanticVersionTests.cs | 498 -------------- .../Service/PackageServiceTests.cs | 146 +++++ .../PackageServiceVersionDetectionTests.cs | 349 ---------- .../ConfigurationExtension.cs | 19 +- .../Models/SoftwareVersion.cs | 97 +++ sh.actions.package-cleanup/Program.cs | 4 +- .../Service/GiteaPackageService.cs | 42 +- .../Service/PackageService.cs | 323 +-------- sh.actions.package-cleanup/Worker.cs | 45 +- 10 files changed, 343 insertions(+), 1797 deletions(-) delete mode 100644 sh.actions.package-cleanup.Tests/Service/PackageServiceDateVersionTests.cs delete mode 100644 sh.actions.package-cleanup.Tests/Service/PackageServiceSemanticVersionTests.cs create mode 100644 sh.actions.package-cleanup.Tests/Service/PackageServiceTests.cs delete mode 100644 sh.actions.package-cleanup.Tests/Service/PackageServiceVersionDetectionTests.cs create mode 100644 sh.actions.package-cleanup/Models/SoftwareVersion.cs diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceDateVersionTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceDateVersionTests.cs deleted file mode 100644 index 8f45fcb..0000000 --- a/sh.actions.package-cleanup.Tests/Service/PackageServiceDateVersionTests.cs +++ /dev/null @@ -1,617 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Moq; -using sh.actions.package_cleanup.Models; -using sh.actions.package_cleanup.Service; -using Xunit; - -namespace sh.actions.package_cleanup.Tests.Service; - -public class PackageServiceDateVersionTests -{ - private Mock _configurationMock; - private Mock _giteaPackageServiceMock; - private IPackageService _packageService; - - private void SetupMocks() - { - _configurationMock = new Mock(); - _giteaPackageServiceMock = new Mock(); - _packageService = new PackageService(_configurationMock.Object, _giteaPackageServiceMock.Object); - } - - // ...existing code... - - #region Version Detection Tests - - [Theory] - [InlineData("2024.12.25.1")] - [InlineData("2025.2.14.1")] - [InlineData("2026.1.1.100")] - [InlineData("2023.12.31.999")] - public void FilterPackagesToDelete_WithDateVersions_ShouldIdentifyCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Single prod date version should not be deleted - Assert.NotNull(result); - Assert.Empty(result); - } - - [Theory] - [InlineData("2024.12.25.1-dev")] - [InlineData("2025.2.14.1-alpha")] - [InlineData("2026.1.1.100-beta")] - [InlineData("2023.12.31.999-rc")] - public void FilterPackagesToDelete_WithDateVersionsAndSuffixes_ShouldIdentifyCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Single prerelease/dev date version should be kept - Assert.NotNull(result); - Assert.Empty(result); - } - - #endregion - - #region Prod Release Tests - Date Versioning - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithOnlyProdReleases_ShouldReturnEmpty() - { - // Arrange - CLEAN_PROD_VERSIONS not set, defaults to false - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2025.12.20.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-30) - }, - new() - { - Id = 2, Name = "test", Version = "2025.12.25.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-20) - }, - new() - { - Id = 3, Name = "test", Version = "2026.1.5.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) - } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Prod releases should be protected by default (CLEAN_PROD_VERSIONS=false) - Assert.Empty(result); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithProdReleasesAndCleaningEnabled_ShouldDeleteOldest() - { - // Arrange - SetupMocks(); - _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("true"); - _configurationMock.Setup(c => c["KEEP_PROD_VERSIONS"]).Returns("2"); - - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2025.1.1.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-365) - }, - new() - { - Id = 2, Name = "test", Version = "2025.6.15.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-200) - }, - new() - { - Id = 3, Name = "test", Version = "2026.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) - }, - new() { Id = 4, Name = "test", Version = "2026.2.14.1", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete oldest 2 (2025.1.1 and 2025.6.15), keep newest 2 - Assert.Equal(2, result.Count); - Assert.Contains(result, p => p.Id == 1); - Assert.Contains(result, p => p.Id == 2); - Assert.DoesNotContain(result, p => p.Id == 3 || p.Id == 4); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithMultipleProdVersions_ShouldKeepNewest() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2025.1.1.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-365) - }, - new() - { - Id = 2, Name = "test", Version = "2025.6.15.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-200) - }, - new() - { - Id = 3, Name = "test", Version = "2026.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) - }, - new() { Id = 4, Name = "test", Version = "2026.2.14.1", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default is CLEAN_PROD_VERSIONS=false, so nothing should be deleted - Assert.Empty(result); - } - - #endregion - - #region Prerelease Tests - Date Versioning - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithPrereleaseVersions_ShouldKeepLatest() - { - // Arrange - Default KEEP_PRERELEASE_VERSIONS=3 - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2026.2.10.1-alpha", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-5) - }, - new() - { - Id = 2, Name = "test", Version = "2026.2.11.1-beta", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-4) - }, - new() - { - Id = 3, Name = "test", Version = "2026.2.12.1-rc", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-3) - }, - new() - { - Id = 4, Name = "test", Version = "2026.2.13.1-preview", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-2) - } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete oldest 1 (alpha), keep newest 3 - Assert.Single(result); - Assert.Contains(result, p => p.Id == 1); // alpha is oldest - Assert.DoesNotContain(result, p => p.Id == 2 || p.Id == 3 || p.Id == 4); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithManyPrereleases_ShouldEnforceLimit() - { - // Arrange - Custom KEEP_PRERELEASE_VERSIONS=2 - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); - - var packages = new List(); - for (int i = 0; i < 10; i++) - { - var date = DateTime.UtcNow.AddDays(-i); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.{date.Day}.{i}-beta", - Type = "docker", - CreatedAt = date - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete 8 oldest, keep only 2 newest (Id=0,1) - Assert.Equal(8, result.Count); - Assert.DoesNotContain(result, p => p.Id < 2); - Assert.Contains(result, p => p.Id >= 2); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithMixedPrereleases_ShouldOrderByDate() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2025.12.1.1-beta", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-60) - }, - new() - { - Id = 2, Name = "test", Version = "2026.1.15.1-beta", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-30) - }, - new() - { - Id = 3, Name = "test", Version = "2026.2.1.1-alpha", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-14) - } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 3, so all should be kept - Assert.Empty(result); - } - - #endregion - - #region Dev Release Tests - Date Versioning - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithDevVersions_ShouldKeepLatest() - { - // Arrange - Default KEEP_DEV_VERSIONS=5 - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2026.2.10.1-dev", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-5) - }, - new() - { - Id = 2, Name = "test", Version = "2026.2.11.2-dev", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-4) - }, - new() - { - Id = 3, Name = "test", Version = "2026.2.12.3-dev", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-3) - } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 5, so all 3 should be kept - Assert.Empty(result); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithManyDailyDevReleases_ShouldEnforceLimit() - { - // Arrange - Custom KEEP_DEV_VERSIONS=3 - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("3"); - - var packages = new List(); - for (int i = 0; i < 30; i++) - { - var date = DateTime.UtcNow.AddDays(-i); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.{date.Day}.{i}-dev", - Type = "docker", - CreatedAt = date - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete 27 oldest, keep only 3 newest (Id=0,1,2) - Assert.Equal(27, result.Count); - Assert.DoesNotContain(result, p => p.Id < 3); - Assert.Contains(result, p => p.Id >= 3); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithManyDailyDevReleasesDefaultLimit_ShouldIdentifyAll() - { - // Arrange - SetupMocks(); - var packages = new List(); - for (int i = 0; i < 30; i++) - { - var date = DateTime.UtcNow.AddDays(-i); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.{date.Day}.{i}-dev", - Type = "docker", - CreatedAt = date - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 5, so 25 should be deleted - Assert.Equal(25, result.Count); - Assert.DoesNotContain(result, p => p.Id < 5); // Keep newest 5 (Id 0-4) - Assert.Contains(result, p => p.Id >= 5); - } - - #endregion - - #region Mixed Release Types - Date Versioning - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithMixedReleaseTypes_ShouldSeparateAndCleanCorrectly() - { - // Arrange - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("2"); - - var packages = new List - { - // Prod releases (should not be deleted - CLEAN_PROD_VERSIONS=false) - new() - { - Id = 1, Name = "test", Version = "2025.12.1.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-80) - }, - new() - { - Id = 2, Name = "test", Version = "2026.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) - }, - // Prerelease (keep 2 newest) - new() - { - Id = 3, Name = "test", Version = "2026.1.25.1-alpha", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-20) - }, - new() - { - Id = 4, Name = "test", Version = "2026.2.1.1-beta", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-14) - }, - new() - { - Id = 5, Name = "test", Version = "2026.2.5.1-rc", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-10) - }, - // Dev (keep 2 newest) - new() - { - Id = 6, Name = "test", Version = "2026.2.10.1-dev", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-5) - }, - new() { Id = 7, Name = "test", Version = "2026.2.14.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.Equal(1, result.Count); // Should delete 1 prerelease (alpha), 0 dev (keep both) - Assert.DoesNotContain(result, p => p.Id <= 2); // Prod not deleted - Assert.DoesNotContain(result, p => p.Id >= 6); // Keep 2 latest dev - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithLargeSet_ShouldHandleCorrectly() - { - // Arrange - SetupMocks(); - _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("false"); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("3"); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("5"); - - var packages = new List(); - - // Add prod releases - for (int i = 0; i < 5; i++) - { - var date = DateTime.UtcNow.AddMonths(-(5 - i)); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.1.1", - Type = "docker", - CreatedAt = date - }); - } - - // Add prerelease - for (int i = 5; i < 15; i++) - { - var date = DateTime.UtcNow.AddDays(-(15 - i) * 7); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.{date.Day}.1-beta", - Type = "docker", - CreatedAt = date - }); - } - - // Add dev - for (int i = 15; i < 30; i++) - { - var date = DateTime.UtcNow.AddDays(-(30 - i)); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.{date.Day}.{(30 - i)}-dev", - Type = "docker", - CreatedAt = date - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.NotNull(result); - // Prod releases should never be deleted - Assert.DoesNotContain(result, p => p.Id < 5); - // Should delete 7 oldest prerelease (keep 3 newest) - var prereleaseDeleted = result.Where(p => p.Id >= 5 && p.Id < 15).ToList(); - Assert.Equal(7, prereleaseDeleted.Count); - // Should delete 10 oldest dev (keep 5 newest) - var devDeleted = result.Where(p => p.Id >= 15).ToList(); - Assert.Equal(10, devDeleted.Count); - } - - #endregion - - #region Edge Cases - Date Versioning - - [Theory] - [InlineData("1000.1.1.1")] // Minimum valid year - [InlineData("2999.12.31.999")] // Maximum valid year - [InlineData("2025.2.14.0")] // Build number 0 - public void FilterPackagesToDeleteByDateVersion_WithBoundaryVersions_ShouldHandleCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.NotNull(result); - } - - [Theory] - [InlineData("999.12.31.1")] // Year too small - [InlineData("3000.1.1.1")] // Year too large - [InlineData("2025.13.1.1")] // Month out of range - [InlineData("2025.0.1.1")] // Month zero - [InlineData("2025.2.32.1")] // Day out of range - [InlineData("2025.2.0.1")] // Day zero - public void FilterPackagesToDelete_WithInvalidDateVersions_ShouldNotProcess(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow }, - new() { Id = 2, Name = "test", Version = "1.0.0.1", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Invalid date version should be skipped, only semantic version processed - Assert.NotNull(result); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithConsecutiveDates_ShouldOrderCorrectly() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2025.2.10.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) - }, - new() - { - Id = 2, Name = "test", Version = "2025.2.11.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) - }, - new() - { - Id = 3, Name = "test", Version = "2025.2.12.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) - }, - new() - { - Id = 4, Name = "test", Version = "2025.2.13.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-2) - }, - new() { Id = 5, Name = "test", Version = "2025.2.14.1", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.NotNull(result); - } - - #endregion - - #region Mixed Versioning Variants - - [Fact] - public void FilterPackagesToDelete_WithMixedVersioningVariants_ShouldProcessBothTypes() - { - // Arrange - SetupMocks(); - var packages = new List - { - // Semantic versioning - new() - { - Id = 1, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) - }, - new() - { - Id = 2, Name = "test", Version = "1.1.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) - }, - // Date versioning - new() - { - Id = 3, Name = "test", Version = "2025.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) - }, - new() - { - Id = 4, Name = "test", Version = "2025.2.10.1-dev", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-5) - } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.NotNull(result); - // Prod releases should not be in delete list - Assert.DoesNotContain(result, p => p.Id == 1 || p.Id == 3); - } - - #endregion -} \ No newline at end of file diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceSemanticVersionTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceSemanticVersionTests.cs deleted file mode 100644 index 0bb03da..0000000 --- a/sh.actions.package-cleanup.Tests/Service/PackageServiceSemanticVersionTests.cs +++ /dev/null @@ -1,498 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Moq; -using sh.actions.package_cleanup.Models; -using sh.actions.package_cleanup.Service; -using Xunit; - -namespace sh.actions.package_cleanup.Tests.Service; - -public class PackageServiceSemanticVersionTests -{ - private Mock _configurationMock; - private Mock _giteaPackageServiceMock; - private IPackageService _packageService; - - private void SetupMocks() - { - _configurationMock = new Mock(); - _giteaPackageServiceMock = new Mock(); - _packageService = new PackageService(_configurationMock.Object, _giteaPackageServiceMock.Object); - } - - #region Version Detection Tests - - [Theory] - [InlineData("1.0.0.0")] - [InlineData("2.5.3.1")] - [InlineData("10.20.30.100")] - [InlineData("1.0.0")] - [InlineData("2.5")] - public void FilterPackagesToDelete_WithSemanticVersions_ShouldIdentifyCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Single prod version should not be deleted (CLEAN_PROD_VERSIONS=false by default) - Assert.NotNull(result); - Assert.Empty(result); - } - - [Theory] - [InlineData("1.0.0.0-dev")] - [InlineData("2.5.3.1-alpha")] - [InlineData("10.20.30.100-beta")] - [InlineData("1.0.0-rc1")] - [InlineData("2.5-preview")] - public void FilterPackagesToDelete_WithSemanticVersionsAndSuffixes_ShouldIdentifyCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Single prerelease/dev should be kept - Assert.NotNull(result); - Assert.Empty(result); - } - - #endregion - - #region Prod Release Tests - Semantic Versioning - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithOnlyProdReleases_ShouldReturnEmpty() - { - // Arrange - CLEAN_PROD_VERSIONS not set, defaults to false - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, - new() { Id = 2, Name = "test", Version = "1.1.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, - new() { Id = 3, Name = "test", Version = "1.2.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Prod releases should be protected by default (CLEAN_PROD_VERSIONS=false) - Assert.Empty(result); - Assert.DoesNotContain(result, p => p.Id == 1 || p.Id == 2 || p.Id == 3); - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithProdReleasesAndCleaningEnabled_ShouldDeleteOldest() - { - // Arrange - SetupMocks(); - _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("true"); - _configurationMock.Setup(c => c["KEEP_PROD_VERSIONS"]).Returns("2"); - - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, - new() { Id = 2, Name = "test", Version = "1.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, - new() { Id = 3, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, - new() { Id = 4, Name = "test", Version = "2.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete oldest 2 (1.0.0.0 and 1.1.0.0), keep newest 2 (2.0.0.0 and 2.1.0.0) - Assert.Equal(2, result.Count); - Assert.Contains(result, p => p.Id == 1); - Assert.Contains(result, p => p.Id == 2); - Assert.DoesNotContain(result, p => p.Id == 3 || p.Id == 4); - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithMultipleProdVersions_ShouldKeepNewest() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, - new() { Id = 2, Name = "test", Version = "1.0.1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, - new() { Id = 3, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, - new() { Id = 4, Name = "test", Version = "2.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default is CLEAN_PROD_VERSIONS=false, so nothing should be deleted - Assert.Empty(result); - } - - #endregion - - #region Prerelease Tests - Semantic Versioning - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithPrereleaseVersions_ShouldKeepLatest() - { - // Arrange - Default KEEP_PRERELEASE_VERSIONS=3 - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, - new() { Id = 2, Name = "test", Version = "1.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, - new() { Id = 3, Name = "test", Version = "1.0.0-rc1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) }, - new() { Id = 4, Name = "test", Version = "1.0.0-preview", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-2) } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete oldest 1 (alpha, id=1), keep newest 3 (beta, rc1, preview) - Assert.Single(result); - Assert.Contains(result, p => p.Id == 1); // alpha is oldest, should be deleted - Assert.DoesNotContain(result, p => p.Id == 2 || p.Id == 3 || p.Id == 4); // Keep newest 3 - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithManyPrereleases_ShouldEnforceLimit() - { - // Arrange - Custom KEEP_PRERELEASE_VERSIONS=2 - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); - - var packages = new List(); - for (int i = 0; i < 10; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"1.0.0-beta{i}", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-(10 - i)) - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete 8 oldest, keep only 2 newest (Id=8,9) - Assert.Equal(8, result.Count); - Assert.DoesNotContain(result, p => p.Id >= 8); - Assert.Contains(result, p => p.Id < 8); - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithMixedPrereleases_ShouldOrderByVersion() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, - new() { Id = 2, Name = "test", Version = "2.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, - new() { Id = 3, Name = "test", Version = "1.5.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 3, so all should be kept - Assert.Empty(result); - } - - #endregion - - #region Dev Release Tests - Semantic Versioning - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithDevVersions_ShouldKeepLatest() - { - // Arrange - Default KEEP_DEV_VERSIONS=5 - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, - new() { Id = 2, Name = "test", Version = "1.0.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, - new() { Id = 3, Name = "test", Version = "2.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 5, so all 3 should be kept - Assert.Empty(result); - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithManyDevReleases_ShouldEnforceLimit() - { - // Arrange - Custom KEEP_DEV_VERSIONS=3 - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("3"); - - var packages = new List(); - for (int i = 0; i < 10; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"1.0.{i}-dev", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-i) - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete 7 oldest, keep only 3 newest (Id=7,8,9 are newest by CreatedAt) - Assert.Equal(7, result.Count); - Assert.DoesNotContain(result, p => p.Id >= 7); // Keep newest 3 - Assert.Contains(result, p => p.Id < 7); // Delete oldest 7 - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithManyDevReleases_ShouldIdentifyAll() - { - // Arrange - SetupMocks(); - var packages = new List(); - for (int i = 0; i < 20; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"1.0.{i}-dev", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-i) - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 5, so 15 should be deleted - Assert.Equal(15, result.Count); - Assert.DoesNotContain(result, p => p.Id >= 15); // Keep newest 5 (Id 15-19) - Assert.Contains(result, p => p.Id < 15); - } - - #endregion - - #region Mixed Release Types - Semantic Versioning - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithMixedReleaseTypes_ShouldSeparateAndCleanCorrectly() - { - // Arrange - CLEAN_PROD_VERSIONS=false, KEEP_PRERELEASE_VERSIONS=2, KEEP_DEV_VERSIONS=2 - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("2"); - - var packages = new List - { - // Prod releases (should not be deleted - CLEAN_PROD_VERSIONS=false) - new() { Id = 1, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, - new() { Id = 2, Name = "test", Version = "1.1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-25) }, - // Prerelease (keep 2 newest) - new() { Id = 3, Name = "test", Version = "1.2.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, - new() { Id = 4, Name = "test", Version = "1.2.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-15) }, - new() { Id = 5, Name = "test", Version = "1.2.1-rc", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, - // Dev (keep 2 newest) - new() { Id = 6, Name = "test", Version = "1.2.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, - new() { Id = 7, Name = "test", Version = "1.2.2-dev", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.Equal(1, result.Count); // Should delete 1 prerelease (alpha), 0 dev (keep both) - Assert.DoesNotContain(result, p => p.Id <= 2); // Prod not deleted - Assert.DoesNotContain(result, p => p.Id >= 6); // Keep 2 latest dev - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithLargeSet_ShouldHandleCorrectly() - { - // Arrange - SetupMocks(); - _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("false"); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("3"); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("5"); - - var packages = new List(); - - // Add prod releases - for (int i = 0; i < 5; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"1.{i}.0", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-(5 - i)) - }); - } - - // Add prerelease - for (int i = 5; i < 15; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"2.0.{i - 5}-beta", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-(15 - i)) - }); - } - - // Add dev - for (int i = 15; i < 30; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"2.1.{i - 15}-dev", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-(30 - i)) - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.NotNull(result); - // Prod releases should never be deleted - Assert.DoesNotContain(result, p => p.Id < 5); - // Should delete 7 oldest prerelease (keep 3 newest) - var prereleaseDeleted = result.Where(p => p.Id >= 5 && p.Id < 15).ToList(); - Assert.Equal(7, prereleaseDeleted.Count); - // Should delete 10 oldest dev (keep 5 newest) - var devDeleted = result.Where(p => p.Id >= 15).ToList(); - Assert.Equal(10, devDeleted.Count); - } - - #endregion - - #region Edge Cases - Semantic Versioning - - [Theory] - [InlineData("0.0.0.0")] - [InlineData("99.99.99.99")] - [InlineData("100.0.0.0")] // Boundary case - might be detected as date version - public void FilterPackagesToDeleteBySemanticVersion_WithBoundaryVersions_ShouldHandleCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should not throw, and boundary versions should be handled properly - Assert.NotNull(result); - // Versions 0.0.0.0 and 99.99.99.99 are semantic, 100.0.0.0 is likely treated as date (will fail date validation) - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithVersionsHavingDifferentLengths_ShouldHandleCorrectly() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, - new() { Id = 2, Name = "test", Version = "1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, - new() { Id = 3, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) }, - new() { Id = 4, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-2) } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should not throw and should not delete any prod versions - Assert.NotNull(result); - // CLEAN_PROD_VERSIONS=false by default, so all versions should be kept - // Id=1 is invalid (only 1 component), ids 2-4 are valid but prod - var validVersions = new[] { 2, 3, 4 }; - foreach (var id in validVersions) - { - Assert.DoesNotContain(result, p => p.Id == id); - } - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithZeroKeepCount_ShouldKeepAll() - { - // Arrange - KEEP_DEV_VERSIONS=0 means keep all - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("0"); - - var packages = new List(); - for (int i = 0; i < 10; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"1.0.{i}-dev", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-i) - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should keep all (0 means infinite/keep all) - Assert.Empty(result); - Assert.DoesNotContain(result, p => p.Id >= 0); // No packages should be deleted - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithInvalidConfigValues_ShouldUseDefaults() - { - // Arrange - Set invalid config values - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("invalid"); - _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("maybe"); - - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, - new() { Id = 2, Name = "test", Version = "1.0.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow }, - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should use defaults (KEEP_DEV_VERSIONS=5, CLEAN_PROD_VERSIONS=false) - // With 2 packages and keep 5, no deletion should happen - Assert.Empty(result); - Assert.DoesNotContain(result, p => p.Id == 1 || p.Id == 2); - } - - #endregion -} - diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceTests.cs new file mode 100644 index 0000000..c2bbec0 --- /dev/null +++ b/sh.actions.package-cleanup.Tests/Service/PackageServiceTests.cs @@ -0,0 +1,146 @@ +using Moq; +using sh.actions.package_cleanup.Models; +using sh.actions.package_cleanup.Service; +using Xunit; + +namespace sh.actions.package_cleanup.Tests.Service; + +public class PackageFilterTests +{ + private readonly IPackageService _packageService; + private readonly Mock _giteaPackageServiceMock = new Mock(); + private readonly List _packages; + + + public PackageFilterTests() + { + _packageService = new PackageService(_giteaPackageServiceMock.Object); + _packages = _versions.Select(v => new GiteaPackage + { + Version = v, + Name = $"test-{v}" + }).ToList(); + } + + [Fact] + public void TestFilterPackagesToDelete() + { + var toDelete = _packageService.FilterPackagesToDelete(_packages); + var toDeleteVersions = toDelete + .Select(x => x.Version) + .OrderByDescending(x => x) + .ToList(); + var expected = _expectDeleted.OrderByDescending(x => x).ToList(); + Assert.Equal(expected.Count, toDeleteVersions.Count); + Assert.Equal(expected, toDeleteVersions); + } + + private readonly List _versions = + [ + "0.1.0", + "0.1.1", + "0.1.2", + "1.0", + "1.1", + "1.2", + "2024.8.6.0", + "2024.8.7.0", + "2024.8.8.0", + "2024.8.9.0", + "2024.8.10.0", + "2024.8.11.0", + "latest", + // dev + "0-dev", + "1-dev", + "2-dev", + "1.0-dev", + "1.1-dev", + "1.2-dev", + "0.1.0-dev", + "0.1.1-dev", + "0.1.2-dev", + "2024.8.6.0-dev", + "2024.8.6.1-dev", + "2024.8.7.0-dev", + "2024.8.7.1-dev", + "2024.8.8.0-dev", + "2024.8.8.1-dev", + "2024.8.9.0-dev", + "2024.8.9.1-dev", + "2024.8.10.0-dev", + "2024.8.10.1-dev", + "2024.8.11.0-dev", + "2024.8.11.1-dev", + // test + "0.1.0-test", + "0.1.1-test", + "0.1.2-test", + "2024.8.6.0-test", + "2024.8.6.1-test", + "2024.8.7.0-test", + "2024.8.7.1-test", + "2024.8.8.0-test", + "2024.8.8.1-test", + "2024.8.9.0-test", + "2024.8.9.1-test", + "2024.8.10.0-test", + "2024.8.10.1-test", + "2024.8.11.0-test", + // exp + "0.1.0-exp", + "0.1.1-exp", + "0.1.2-exp", + "2024.8.6.0-exp", + "2024.8.6.1-exp", + "2024.8.7.0-exp", + "2024.8.7.1-exp", + "2024.8.8.0-exp", + "2024.8.8.1-exp", + "2024.8.9.0-exp", + "2024.8.9.1-exp", + "2024.8.10.0-exp", + "2024.8.10.1-exp", + "2024.8.11.0-exp", + "2024.8.11.2-exp" + ]; + + private readonly List _versionsToHold = + [ + // prod (no suffix) + "0.1.0", + "0.1.1", + "0.1.2", + "1.0", + "1.1", + "1.2", + "2024.8.6.0", + "2024.8.7.0", + "2024.8.8.0", + "2024.8.9.0", + "2024.8.10.0", + "2024.8.11.0", + "latest", + // dev + "1-dev", + "2-dev", + "1.1-dev", + "1.2-dev", + "0.1.1-dev", + "0.1.2-dev", + "2024.8.11.0-dev", + "2024.8.11.1-dev", + // test + "0.1.1-test", + "0.1.2-test", + "2024.8.10.1-test", + "2024.8.11.0-test", + // exp + "0.1.1-exp", + "0.1.2-exp", + "2024.8.11.0-exp", + "2024.8.11.2-exp" + ]; + + private List _expectDeleted => _versions.Except(_versionsToHold).ToList(); +} \ No newline at end of file diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceVersionDetectionTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceVersionDetectionTests.cs deleted file mode 100644 index f8eb400..0000000 --- a/sh.actions.package-cleanup.Tests/Service/PackageServiceVersionDetectionTests.cs +++ /dev/null @@ -1,349 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Moq; -using sh.actions.package_cleanup.Models; -using sh.actions.package_cleanup.Service; -using Xunit; - -namespace sh.actions.package_cleanup.Tests.Service; - -public class PackageServiceVersionDetectionTests -{ - private readonly IPackageService _packageService; - private readonly Mock _configurationMock; - private readonly Mock _giteaPackageServiceMock; - - public PackageServiceVersionDetectionTests() - { - _configurationMock = new Mock(); - _giteaPackageServiceMock = new Mock(); - _packageService = new PackageService(_configurationMock.Object, _giteaPackageServiceMock.Object); - } - - #region Release Type Detection Tests - - [Theory] - [InlineData("1.0.0")] - [InlineData("2.5.3")] - [InlineData("10.20.30")] - [InlineData("2025.2.14.1")] - public void FilterPackagesToDelete_WithProdReleases_ShouldIdentifyAsNonDeletable(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Prod releases should not be in delete list (CLEAN_PROD_VERSIONS=false by default) - Assert.DoesNotContain(result, p => p.Id == 1); - Assert.Empty(result); // No packages should be deleted without CLEAN_PROD_VERSIONS=true - } - - [Theory] - [InlineData("1.0.0-alpha")] - [InlineData("1.0.0-ALPHA")] - [InlineData("2.5.3-alpha123")] - [InlineData("1.0.0-beta")] - [InlineData("1.0.0-BETA")] - [InlineData("1.0.0-rc")] - [InlineData("1.0.0-RC1")] - [InlineData("1.0.0-preview")] - [InlineData("1.0.0-PREVIEW")] - public void FilterPackagesToDelete_WithPrereleaseVersions_ShouldIdentifyCorrectly(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Prerelease should be kept (default KEEP_PRERELEASE_VERSIONS=3, so 1 package is kept) - Assert.NotNull(result); - Assert.Empty(result); // Single prerelease should be kept as it's within the limit - } - - [Theory] - [InlineData("1.0.0-dev")] - [InlineData("1.0.0-DEV")] - [InlineData("2.5.3-dev123")] - [InlineData("1.0.0-development")] - [InlineData("1.0.0-DEV")] - public void FilterPackagesToDelete_WithDevVersions_ShouldIdentifyCorrectly(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Dev version should be kept (default KEEP_DEV_VERSIONS=5, so 1 package is kept) - Assert.NotNull(result); - Assert.Empty(result); // Single dev version should be kept as it's within the limit - } - - #endregion - - #region Semantic Version Detection Tests - - [Theory] - [InlineData("1.0")] - [InlineData("1.0.0")] - [InlineData("1.0.0.0")] - [InlineData("2.5")] - [InlineData("2.5.3")] - [InlineData("2.5.3.100")] - [InlineData("0.0.0.0")] - [InlineData("99.99.99.99")] - public void FilterPackagesToDelete_WithValidSemanticVersions_ShouldProcess(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should process without throwing and not delete prod releases - Assert.NotNull(result); - Assert.DoesNotContain(result, p => p.Id == 1); // Prod version should not be deleted - } - - [Theory] - [InlineData("1")] // Only 1 component - [InlineData("1.0.0.0.0")] // 5 components - [InlineData("1.a.0.0")] // Non-numeric - [InlineData("a.b.c.d")] // All non-numeric - public void FilterPackagesToDelete_WithInvalidSemanticVersions_ShouldSkip(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow }, - new() { Id = 2, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Invalid version should be skipped, only valid semantic version (id=2) is processed - Assert.NotNull(result); - // Should not crash and should only consider the valid version - Assert.DoesNotContain(result, p => p.Id == 2); // Valid prod should not be deleted - } - - [Theory] - [InlineData("101.0.0.0")] - [InlineData("200.5.3.100")] - [InlineData("1000.0.0.0")] - public void FilterPackagesToDelete_WithLargeMajorVersion_ShouldTreatAsDateVersion(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Large major versions (>100) are detected as date versions, not semantic - // These will fail date validation (invalid month/day) and be skipped - Assert.NotNull(result); - } - - #endregion - - #region Date Version Detection Tests - - [Theory] - [InlineData("2024.1.1.1")] - [InlineData("2025.2.14.1")] - [InlineData("2026.12.31.999")] - [InlineData("2000.6.15.100")] - [InlineData("1999.1.1.1")] - public void FilterPackagesToDelete_WithValidDateVersions_ShouldProcess(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should process without throwing and not delete prod date versions - Assert.NotNull(result); - Assert.DoesNotContain(result, p => p.Id == 1); // Prod date version should not be deleted - } - - [Theory] - [InlineData("2025.1.1")] // 3 components (missing build) - [InlineData("2025.1.1.1.1")] // 5 components - [InlineData("2025.13.1.1")] // Month out of range - [InlineData("2025.0.1.1")] // Month zero - [InlineData("2025.2.32.1")] // Day out of range - [InlineData("2025.2.0.1")] // Day zero - [InlineData("999.2.14.1")] // Year too small - [InlineData("3000.2.14.1")] // Year too large - [InlineData("2025.a.14.1")] // Non-numeric month - public void FilterPackagesToDelete_WithInvalidDateVersions_ShouldSkip(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow }, - new() { Id = 2, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Invalid date version should be skipped, valid semantic version (id=2) is processed as prod - Assert.NotNull(result); - Assert.DoesNotContain(result, p => p.Id == 2); // Valid semantic prod should not be deleted - } - - [Theory] - [InlineData("1000.1.1.1")] // Minimum valid year - [InlineData("2999.12.31.999")] // Maximum valid year - [InlineData("2025.1.1.1")] // January 1 - [InlineData("2025.12.31.1")] // December 31 - public void FilterPackagesToDelete_WithDateVersionBoundaries_ShouldProcess(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Boundary date versions should be recognized as valid - Assert.NotNull(result); - Assert.DoesNotContain(result, p => p.Id == 1); // Prod date version should not be deleted - } - - #endregion - - #region Case Sensitivity Tests - - [Theory] - [InlineData("1.0.0-alpha")] - [InlineData("1.0.0-ALPHA")] - [InlineData("1.0.0-Alpha")] - [InlineData("1.0.0-AlPhA")] - public void FilterPackagesToDelete_WithDifferentCasePrerelease_ShouldHandleCorrectly(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - All case variations should be recognized as prerelease - Assert.NotNull(result); - Assert.Empty(result); // Single prerelease should be kept - } - - [Theory] - [InlineData("1.0.0-dev")] - [InlineData("1.0.0-DEV")] - [InlineData("1.0.0-Dev")] - [InlineData("1.0.0-DeV")] - public void FilterPackagesToDelete_WithDifferentCaseDev_ShouldHandleCorrectly(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - All case variations should be recognized as dev version - Assert.NotNull(result); - Assert.Empty(result); // Single dev version should be kept - } - - #endregion - - #region Suffix/Identifier Tests - - [Theory] - [InlineData("1.0.0-alpha1")] - [InlineData("1.0.0-alpha.1")] - [InlineData("1.0.0-beta2")] - [InlineData("1.0.0-rc.1")] - [InlineData("2025.2.14.1-dev.1")] - [InlineData("2025.2.14.1-dev123")] - public void FilterPackagesToDelete_WithCompoundSuffixes_ShouldIdentifyCorrectly(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Compound suffixes should be recognized and not cause errors - Assert.NotNull(result); - Assert.Empty(result); // Single prerelease/dev should be kept - } - - #endregion - - #region Empty and Null Tests - - [Fact] - public void FilterPackagesToDelete_WithEmptyList_ShouldReturnEmpty() - { - // Arrange - var packages = new List(); - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Empty input should return empty result - Assert.NotNull(result); - Assert.Empty(result); - } - - [Fact] - public void FilterPackagesToDelete_WithNullableInputs_ShouldHandleGracefully() - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should handle without throwing and not delete prod - Assert.NotNull(result); - Assert.Empty(result); // Prod should not be deleted - } - - #endregion -} - diff --git a/sh.actions.package-cleanup/ConfigurationExtension.cs b/sh.actions.package-cleanup/ConfigurationExtension.cs index d6ca7ca..8bd891e 100644 --- a/sh.actions.package-cleanup/ConfigurationExtension.cs +++ b/sh.actions.package-cleanup/ConfigurationExtension.cs @@ -2,24 +2,25 @@ namespace sh.actions.package_cleanup; public static class ConfigurationExtension { - private static void EnsureVariable(IConfiguration configuration, string key) + private const string MissingConfigurationError = "Configuration variable '{0}' is required but was not found."; + + private static void ValidateConfigurationVariable(IConfiguration configuration, string key) { - var value = configuration[key]; - if (string.IsNullOrEmpty(value)) + if (string.IsNullOrWhiteSpace(configuration[key])) { - throw new InvalidOperationException($"Configuration variable '{key}' is required but was not found."); + throw new InvalidOperationException(string.Format(MissingConfigurationError, key)); } } public static IConfigurationBuilder EnsureGiteaConfig(this IConfigurationBuilder builder) { var configuration = builder.Build(); + var requiredKeys = new[] { "URL", "OWNER", "TYPE", "NAME", "API_TOKEN" }; - EnsureVariable(configuration, "URL"); - EnsureVariable(configuration, "OWNER"); - EnsureVariable(configuration, "TYPE"); - EnsureVariable(configuration, "NAME"); - EnsureVariable(configuration, "API_TOKEN"); + foreach (var key in requiredKeys) + { + ValidateConfigurationVariable(configuration, key); + } return builder; } diff --git a/sh.actions.package-cleanup/Models/SoftwareVersion.cs b/sh.actions.package-cleanup/Models/SoftwareVersion.cs new file mode 100644 index 0000000..6186eab --- /dev/null +++ b/sh.actions.package-cleanup/Models/SoftwareVersion.cs @@ -0,0 +1,97 @@ +using System.Text.RegularExpressions; + +namespace sh.actions.package_cleanup.Models; + +public class SoftwareVersion( + int major, + int minor, + int patch = 0, + int build = 0, + string? suffix = null, + string? input = null, + bool isLatest = false) +{ + public int Major { get; set; } = major; + public int Minor { get; set; } = minor; + public int Patch { get; set; } = patch; + public int Build { get; set; } = build; + public string? Suffix { get; set; } = suffix; + public string? Input { get; set; } = input; + public bool IsLatest { get; set; } = isLatest; + + public override string ToString() + { + var versionStr = $"{Major}.{Minor}.{Patch}.{Build}"; + if (!string.IsNullOrEmpty(Suffix)) + { + versionStr += $"-{Suffix}"; + } + + return versionStr; + } + + public override bool Equals(object? obj) + { + return obj is SoftwareVersion version && ToString() == version.ToString(); + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + public static SoftwareVersion Parse(string versionString) + { + // Try matching format: X.X.X.X or X.X.X.X-suffix + var match = Regex.Match(versionString, @"(\d+)\.(\d+)\.(\d+)\.(\d+)(?:-(.*))?"); + if (match.Success) + { + var major = int.Parse(match.Groups[1].Value); + var minor = int.Parse(match.Groups[2].Value); + var patch = int.Parse(match.Groups[3].Value); + var build = int.Parse(match.Groups[4].Value); + var suffix = match.Groups[5].Value; + return new SoftwareVersion(major, minor, patch, build, string.IsNullOrEmpty(suffix) ? null : suffix, + versionString); + } + + // Try matching format: X.X.X or X.X.X-suffix + match = Regex.Match(versionString, @"(\d+)\.(\d+)\.(\d+)(?:-(.*))?"); + if (match.Success) + { + var major = int.Parse(match.Groups[1].Value); + var minor = int.Parse(match.Groups[2].Value); + var patch = int.Parse(match.Groups[3].Value); + var suffix = match.Groups[4].Value; + return new SoftwareVersion(major, minor, patch, 0, string.IsNullOrEmpty(suffix) ? null : suffix, + versionString); + } + + // Try matching format: X.X or X.X-suffix + match = Regex.Match(versionString, @"(\d+)\.(\d+)(?:-(.*))?"); + if (match.Success) + { + var major = int.Parse(match.Groups[1].Value); + var minor = int.Parse(match.Groups[2].Value); + var suffix = match.Groups[3].Value; + return new SoftwareVersion(major, minor, 0, 0, string.IsNullOrEmpty(suffix) ? null : suffix, versionString); + } + + // Try matching format: X or X-suffix + match = Regex.Match(versionString, @"(\d+)(?:-(.*))?"); + if (match.Success) + { + var major = int.Parse(match.Groups[1].Value); + var suffix = match.Groups[2].Value; + return new SoftwareVersion(major, 0, 0, 0, string.IsNullOrEmpty(suffix) ? null : suffix, versionString); + } + + // Special case for "latest" + if (versionString == "latest") + { + return new SoftwareVersion(1, 0, 0, 0, null, versionString, isLatest: true); + } + + throw new ArgumentException($"Invalid version string: {versionString}"); + } +} \ No newline at end of file diff --git a/sh.actions.package-cleanup/Program.cs b/sh.actions.package-cleanup/Program.cs index be417e2..33f69d3 100644 --- a/sh.actions.package-cleanup/Program.cs +++ b/sh.actions.package-cleanup/Program.cs @@ -15,7 +15,5 @@ builder.Services .AddHostedService() .AddHttpClient(); - var host = builder.Build(); - -host.Run(); \ No newline at end of file +await host.RunAsync(); diff --git a/sh.actions.package-cleanup/Service/GiteaPackageService.cs b/sh.actions.package-cleanup/Service/GiteaPackageService.cs index 1efcff4..4806331 100644 --- a/sh.actions.package-cleanup/Service/GiteaPackageService.cs +++ b/sh.actions.package-cleanup/Service/GiteaPackageService.cs @@ -10,22 +10,33 @@ public class GiteaPackageService( HttpClient httpClient) : IGiteaPackageService { + private string GetBaseUrl() => + $"{configuration["URL"]?.TrimEnd('/')}/packages/{configuration["OWNER"]}/{configuration["TYPE"]}/{configuration["NAME"]}"; + + private void AddAuthorizationHeader(HttpRequestMessage request) + { + var token = configuration["API_TOKEN"]; + if (!string.IsNullOrEmpty(token)) + { + request.Headers.Add("Authorization", $"token {token}"); + } + } + public async Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default) { try { - var baseUrl = - $"{configuration["URL"]?.TrimEnd('/')}/packages/{configuration["OWNER"]}/{configuration["TYPE"]}/{configuration["NAME"]}"; + var baseUrl = GetBaseUrl(); var allPackages = new List(); var page = 1; while (true) { var url = $"{baseUrl}?page={page}"; - logger.LogInformation("Fetching packages from Gitea: {Url}", url); + var request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.Add("Authorization", $"token {configuration["API_TOKEN"]}"); + AddAuthorizationHeader(request); var response = await httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); @@ -46,7 +57,6 @@ public class GiteaPackageService( } logger.LogInformation("Successfully fetched {Count} packages in total", allPackages.Count); - return allPackages; } catch (HttpRequestException ex) @@ -65,30 +75,30 @@ public class GiteaPackageService( { try { - var url = - $"{configuration["URL"]?.TrimEnd('/')}/packages/{configuration["OWNER"]}/{package.Type}/{package.Name}/{package.Version}"; - logger.LogInformation("Deleting package {PackageName} (ID: {PackageId}) from Gitea: {Url}", package.Name, - package.Id, url); + var baseUrl = GetBaseUrl(); + var url = $"{baseUrl}/{package.Version}"; + logger.LogInformation("Deleting package {PackageName} (ID: {PackageId}) from Gitea", + package.Name, package.Id); var request = new HttpRequestMessage(HttpMethod.Delete, url); - request.Headers.Add("Authorization", $"token {configuration["API_TOKEN"]}"); + AddAuthorizationHeader(request); var response = await httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); - logger.LogInformation("Successfully deleted package {PackageName} (ID: {PackageId})", package.Name, - package.Id); + logger.LogInformation("Successfully deleted package {PackageName} (ID: {PackageId})", + package.Name, package.Id); } catch (HttpRequestException ex) { - logger.LogError(ex, "Error deleting package {PackageName} (ID: {PackageId}) from Gitea", package.Name, - package.Id); + logger.LogError(ex, "Error deleting package {PackageName} (ID: {PackageId}) from Gitea", + package.Name, package.Id); throw; } catch (Exception ex) { - logger.LogError(ex, "Unexpected error deleting package {PackageName} (ID: {PackageId})", package.Name, - package.Id); + logger.LogError(ex, "Unexpected error deleting package {PackageName} (ID: {PackageId})", + package.Name, package.Id); throw; } } diff --git a/sh.actions.package-cleanup/Service/PackageService.cs b/sh.actions.package-cleanup/Service/PackageService.cs index 23a37eb..3a03e9a 100644 --- a/sh.actions.package-cleanup/Service/PackageService.cs +++ b/sh.actions.package-cleanup/Service/PackageService.cs @@ -2,7 +2,7 @@ using sh.actions.package_cleanup.Models; namespace sh.actions.package_cleanup.Service; -public class PackageService(IConfiguration configuration, IGiteaPackageService packageService) : IPackageService +public class PackageService(IGiteaPackageService packageService) : IPackageService { public async Task> GetFilteredPackages() { @@ -12,301 +12,40 @@ public class PackageService(IConfiguration configuration, IGiteaPackageService p public List FilterPackagesToDelete(List packages) { - var packagesToDelete = new List(); + var versionMap = packages.ToDictionary(p => p.Version, p => SoftwareVersion.Parse(p.Version)); + var versionsToKeep = new HashSet(); - // Group packages by versioning variant - var semanticVersionPackages = packages.Where(p => IsSemanticVersion(p.Version)).ToList(); - var dateVersionPackages = packages.Where(p => IsDateVersion(p.Version)).ToList(); + foreach (var package in packages + .Where(p => string.IsNullOrEmpty(versionMap[p.Version].Suffix)) + .GroupBy(p => versionMap[p.Version].ToString()) + .Select(g => g.OrderBy(p => p.Version.Length).First())) + { + versionsToKeep.Add(package.Version); + } - // Process each group with their respective variant logic - packagesToDelete.AddRange(FilterPackagesToDeleteBySemanticVersion(semanticVersionPackages)); - packagesToDelete.AddRange(FilterPackagesToDeleteByDateVersion(dateVersionPackages)); + var latestVersion = packages.FirstOrDefault(p => versionMap[p.Version].IsLatest); + if (latestVersion != null) + { + versionsToKeep.Add(latestVersion.Version); + } - return packagesToDelete; - } + var suffixedVersionsToKeep = packages + .Where(p => !string.IsNullOrEmpty(versionMap[p.Version].Suffix) && + !(versionMap[p.Version].Major == 0 && versionMap[p.Version].Minor == 0 && + versionMap[p.Version].Patch == 0)) + .GroupBy(p => (versionMap[p.Version].Suffix, versionMap[p.Version].Major, versionMap[p.Version].Minor)) + .SelectMany(g => g + .OrderByDescending(p => versionMap[p.Version].Patch) + .ThenByDescending(p => versionMap[p.Version].Build) + .Take(2) + .GroupBy(p => versionMap[p.Version].ToString()) + .Select(group => group.OrderBy(p => p.Version.Length).First())); - private List FilterPackagesToDeleteBySemanticVersion(List packages) - { - var packagesToDelete = new List(); + foreach (var package in suffixedVersionsToKeep) + { + versionsToKeep.Add(package.Version); + } - // Group packages by release type - // Note: Dev releases must be checked before prerelease since -dev is a suffix - var devPackages = packages.Where(p => IsDevRelease(p.Version)) - .OrderByDescending(p => ParseSemanticVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - var prereleasePackages = packages.Where(p => IsPrereleaseRelease(p.Version) && !IsDevRelease(p.Version)) - .OrderByDescending(p => ParseSemanticVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - var prodPackages = packages.Where(p => IsProdRelease(p.Version)) - .OrderByDescending(p => ParseSemanticVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - - // Apply cleanup rules for each type - packagesToDelete.AddRange(FilterProdReleases(prodPackages)); - packagesToDelete.AddRange(FilterPrereleaseReleases(prereleasePackages)); - packagesToDelete.AddRange(FilterDevReleases(devPackages)); - - return packagesToDelete; - } - - private List FilterPackagesToDeleteByDateVersion(List packages) - { - var packagesToDelete = new List(); - - // Group packages by release type - // Note: Dev releases must be checked before prerelease since -dev is a suffix - var devPackages = packages.Where(p => IsDevRelease(p.Version)) - .OrderByDescending(p => ParseDateVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - var prereleasePackages = packages.Where(p => IsPrereleaseRelease(p.Version) && !IsDevRelease(p.Version)) - .OrderByDescending(p => ParseDateVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - var prodPackages = packages.Where(p => IsProdRelease(p.Version)) - .OrderByDescending(p => ParseDateVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - - // Apply cleanup rules for each type - packagesToDelete.AddRange(FilterProdReleasesDateVersion(prodPackages)); - packagesToDelete.AddRange(FilterPrereleaseReleasesDateVersion(prereleasePackages)); - packagesToDelete.AddRange(FilterDevReleasesDateVersion(devPackages)); - - return packagesToDelete; - } - - // Prod release methods for semantic versioning - private List FilterProdReleases(List packages) - { - // Check if prod cleaning is enabled - if (!GetBoolConfigValue("CLEAN_PROD_VERSIONS", false)) - return new List(); - - // Get keep count - var keepCount = GetIntConfigValue("KEEP_PROD_VERSIONS", 5); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Prerelease methods for semantic versioning - private List FilterPrereleaseReleases(List packages) - { - // Get keep count - var keepCount = GetIntConfigValue("KEEP_PRERELEASE_VERSIONS", 3); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Dev release methods for semantic versioning - private List FilterDevReleases(List packages) - { - // Get keep count - var keepCount = GetIntConfigValue("KEEP_DEV_VERSIONS", 5); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Prod release methods for date-based versioning - private List FilterProdReleasesDateVersion(List packages) - { - // Check if prod cleaning is enabled - if (!GetBoolConfigValue("CLEAN_PROD_VERSIONS", false)) - return new List(); - - // Get keep count - var keepCount = GetIntConfigValue("KEEP_PROD_VERSIONS", 5); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Prerelease methods for date-based versioning - private List FilterPrereleaseReleasesDateVersion(List packages) - { - // Get keep count - var keepCount = GetIntConfigValue("KEEP_PRERELEASE_VERSIONS", 3); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Dev release methods for date-based versioning - private List FilterDevReleasesDateVersion(List packages) - { - // Get keep count - var keepCount = GetIntConfigValue("KEEP_DEV_VERSIONS", 5); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Helper methods to identify release types - private bool IsProdRelease(string version) - { - // Prod versions typically don't have pre-release identifiers (alpha, beta, rc, dev, etc.) - return !version.Contains("-", StringComparison.OrdinalIgnoreCase); - } - - private bool IsPrereleaseRelease(string version) - { - // Prerelease versions contain identifiers like -alpha, -beta, -rc, -preview - // But NOT -dev, which is a separate release type - var lowerVersion = version.ToLowerInvariant(); - return (lowerVersion.Contains("alpha") || lowerVersion.Contains("beta") || - lowerVersion.Contains("rc") || lowerVersion.Contains("preview")) || - lowerVersion.Contains("staging") && !lowerVersion.Contains("dev"); - } - - private bool IsDevRelease(string version) - { - // Dev versions contain identifiers like dev or similar - return version.ToLowerInvariant().Contains("dev"); - } - - // Helper methods to identify versioning variant - private bool IsSemanticVersion(string version) - { - // Semantic versioning: major.minor.micro.build with typically smaller numbers - // Examples: 1.2.3.4, 2.0.1.100 - var versionPart = version.Split('-')[0]; - var parts = versionPart.Split('.').Where(p => !string.IsNullOrEmpty(p)).ToArray(); - - if (parts.Length < 2 || parts.Length > 4) - return false; - - // Try to parse all parts as numbers - if (!parts.All(p => int.TryParse(p, out var num))) - return false; - - // Semantic versioning typically has smaller major version numbers (0-20 range) - if (int.TryParse(parts[0], out var major) && major > 100) - return false; // Likely a date version if first number > 100 - - return true; - } - - private bool IsDateVersion(string version) - { - // Date-based versioning: year.month.day.build - // Examples: 2024.12.25.1, 2025.2.14.100 - var versionPart = version.Split('-')[0]; - var parts = versionPart.Split('.').Where(p => !string.IsNullOrEmpty(p)).ToArray(); - - if (parts.Length != 4) - return false; - - // Try to parse all parts as numbers - if (!parts.All(p => int.TryParse(p, out var num))) - return false; - - // Date version should have year > 1000 and < 3000 - if (!int.TryParse(parts[0], out var year) || year < 1000 || year > 3000) - return false; - - // Month should be 1-12 - if (!int.TryParse(parts[1], out var month) || month < 1 || month > 12) - return false; - - // Day should be 1-31 - if (!int.TryParse(parts[2], out var day) || day < 1 || day > 31) - return false; - - return true; - } - - // Version parsing methods - private (int major, int minor, int micro, int build) ParseSemanticVersion(string version) - { - // Remove suffix if present (e.g., "-alpha", "-dev") - var versionPart = version.Split('-')[0]; - var parts = versionPart.Split('.').Select(p => int.TryParse(p, out var num) ? num : 0).ToArray(); - - return ( - parts.Length > 0 ? parts[0] : 0, - parts.Length > 1 ? parts[1] : 0, - parts.Length > 2 ? parts[2] : 0, - parts.Length > 3 ? parts[3] : 0 - ); - } - - private (int year, int month, int day, int build) ParseDateVersion(string version) - { - // Remove suffix if present (e.g., "-alpha", "-dev") - var versionPart = version.Split('-')[0]; - var parts = versionPart.Split('.').Select(p => int.TryParse(p, out var num) ? num : 0).ToArray(); - - return ( - parts.Length > 0 ? parts[0] : 0, - parts.Length > 1 ? parts[1] : 0, - parts.Length > 2 ? parts[2] : 0, - parts.Length > 3 ? parts[3] : 0 - ); - } - - // Configuration helper methods - private int GetIntConfigValue(string key, int defaultValue) - { - var value = configuration[key]; - if (string.IsNullOrEmpty(value)) - return defaultValue; - - if (int.TryParse(value, out var result)) - return result; - - return defaultValue; - } - - private bool GetBoolConfigValue(string key, bool defaultValue) - { - var value = configuration[key]; - if (string.IsNullOrEmpty(value)) - return defaultValue; - - if (bool.TryParse(value, out var result)) - return result; - - // Handle common string representations - if (value.Equals("true", StringComparison.OrdinalIgnoreCase) || - value.Equals("1", StringComparison.OrdinalIgnoreCase)) - return true; - - return defaultValue; + return packages.Where(p => !versionsToKeep.Contains(p.Version)).ToList(); } } \ No newline at end of file diff --git a/sh.actions.package-cleanup/Worker.cs b/sh.actions.package-cleanup/Worker.cs index d4d857f..188db1c 100644 --- a/sh.actions.package-cleanup/Worker.cs +++ b/sh.actions.package-cleanup/Worker.cs @@ -13,29 +13,48 @@ public class Worker( { private async Task DeletePackages(List packages, CancellationToken cancellationToken = default) { - if (configuration["DRY_RUN"]?.ToLower() == "true") + var dryRun = configuration["DRY_RUN"]?.ToLower() == "true"; + if (dryRun) { - logger.LogInformation("Dry run enabled, not deleting packages"); + logger.LogInformation("Dry run enabled, not deleting {Count} packages", packages.Count); return; } foreach (var giteaPackage in packages) { - await giteaPackageService.DeletePackage(giteaPackage, cancellationToken); + try + { + await giteaPackageService.DeletePackage(giteaPackage, cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to delete package {PackageName} version {Version}", + giteaPackage.Name, giteaPackage.Version); + } } } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - var packages = await packageService.GetFilteredPackages(); - Console.WriteLine($"Found {packages.Count()} packages"); - - var packagesToDelete = packageService.FilterPackagesToDelete(packages); - Console.WriteLine($"Found {packagesToDelete.Count()} packages to delete"); - - await DeletePackages(packagesToDelete, cancellationToken); - - logger.LogInformation("Cleanup finished, stopping application"); - appLifetime.StopApplication(); + try + { + var packages = await packageService.GetFilteredPackages(); + logger.LogInformation("Found {Count} packages", packages.Count); + + var packagesToDelete = packageService.FilterPackagesToDelete(packages); + logger.LogInformation("Found {Count} packages to delete", packagesToDelete.Count); + + await DeletePackages(packagesToDelete, cancellationToken); + + logger.LogInformation("Cleanup finished successfully"); + } + catch (Exception ex) + { + logger.LogError(ex, "Cleanup failed with an error"); + } + finally + { + appLifetime.StopApplication(); + } } } \ No newline at end of file From ca51f738c0246e3edd86912e2288ca6ba690026e Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 03:20:03 +0100 Subject: [PATCH 19/37] Some improvements --- package-cleanup/action.yaml | 39 ++++++++--- .../ConfigurationExtension.cs | 2 +- .../Service/GiteaPackageService.cs | 69 +++++++++++++------ 3 files changed, 80 insertions(+), 30 deletions(-) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index dac5a53..68bdc74 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -1,19 +1,42 @@ name: "package cleanup" description: "Cleans up old packages and versions" +inputs: + url: + description: "Server URL" + required: true + names: + description: "Names of packages" + required: true + owner: + description: "Owner of the package" + required: true + types: + description: "Types of packages (e.g. Container, PyPi, NuGet)" + required: false + default: "Container,PyPi,NuGet" + api_token: + description: "API token for authentication" + required: true + runs: using: "composite" steps: - - name: Install set-version tool + - name: Download and test package-cleanup tool shell: bash run: | - dotnet tool install \ - --tool-path .tools \ - ShEdraft.SetVersion \ - --version 1.0.0 \ - --add-source https://git.sh-edraft.de/api/packages/sh-edraft.de/nuget/index.json + curl -OJ https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/package-cleanup-linux-x64 - - name: Run set-version + # Make executable + chmod +x package-cleanup-linux-x64 + + - name: Run package-cleanup shell: bash + env: + URL: ${{ inputs.url }} + OWNER: ${{ inputs.owner }} + TYPES: ${{ inputs.types }} + NAMES: ${{ inputs.names }} + API_TOKEN: ${{ inputs.api_token }} run: | - ./.tools/set-version --suffix dev + ./package-cleanup-linux-x64 diff --git a/sh.actions.package-cleanup/ConfigurationExtension.cs b/sh.actions.package-cleanup/ConfigurationExtension.cs index 8bd891e..e248316 100644 --- a/sh.actions.package-cleanup/ConfigurationExtension.cs +++ b/sh.actions.package-cleanup/ConfigurationExtension.cs @@ -15,7 +15,7 @@ public static class ConfigurationExtension public static IConfigurationBuilder EnsureGiteaConfig(this IConfigurationBuilder builder) { var configuration = builder.Build(); - var requiredKeys = new[] { "URL", "OWNER", "TYPE", "NAME", "API_TOKEN" }; + var requiredKeys = new[] { "URL", "OWNER", "TYPES", "NAME", "API_TOKEN" }; foreach (var key in requiredKeys) { diff --git a/sh.actions.package-cleanup/Service/GiteaPackageService.cs b/sh.actions.package-cleanup/Service/GiteaPackageService.cs index 4806331..3a95789 100644 --- a/sh.actions.package-cleanup/Service/GiteaPackageService.cs +++ b/sh.actions.package-cleanup/Service/GiteaPackageService.cs @@ -11,7 +11,7 @@ public class GiteaPackageService( : IGiteaPackageService { private string GetBaseUrl() => - $"{configuration["URL"]?.TrimEnd('/')}/packages/{configuration["OWNER"]}/{configuration["TYPE"]}/{configuration["NAME"]}"; + $"{configuration["URL"]?.TrimEnd('/')}/packages/{configuration["OWNER"]}"; private void AddAuthorizationHeader(HttpRequestMessage request) { @@ -28,32 +28,59 @@ public class GiteaPackageService( { var baseUrl = GetBaseUrl(); var allPackages = new List(); - var page = 1; + + // Parse comma-separated types + var types = (configuration["TYPE"] ?? "") + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - while (true) + if (types.Length == 0) { - var url = $"{baseUrl}?page={page}"; - logger.LogInformation("Fetching packages from Gitea: {Url}", url); + logger.LogWarning("No package types configured"); + return allPackages; + } - var request = new HttpRequestMessage(HttpMethod.Get, url); - AddAuthorizationHeader(request); + // Parse comma-separated names + var names = (configuration["NAMES"] ?? "") + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - var response = await httpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); + if (names.Length == 0) + { + logger.LogWarning("No package names configured"); + return allPackages; + } - var packages = - await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken) - ?? []; - - if (packages.Count == 0) + foreach (var type in types) + { + foreach (var name in names) { - break; + var page = 1; + + while (true) + { + var url = $"{baseUrl}/{type}/{name}?page={page}"; + logger.LogInformation("Fetching packages from Gitea: {Url}", url); + + var request = new HttpRequestMessage(HttpMethod.Get, url); + AddAuthorizationHeader(request); + + var response = await httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + + var packages = + await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken) + ?? []; + + if (packages.Count == 0) + { + break; + } + + allPackages.AddRange(packages); + logger.LogInformation("Fetched {Count} packages from page {Page}", packages.Count, page); + + page++; + } } - - allPackages.AddRange(packages); - logger.LogInformation("Fetched {Count} packages from page {Page}", packages.Count, page); - - page++; } logger.LogInformation("Successfully fetched {Count} packages in total", allPackages.Count); @@ -76,7 +103,7 @@ public class GiteaPackageService( try { var baseUrl = GetBaseUrl(); - var url = $"{baseUrl}/{package.Version}"; + var url = $"{baseUrl}/{package.Type}/{package.Name}/{package.Version}"; logger.LogInformation("Deleting package {PackageName} (ID: {PackageId}) from Gitea", package.Name, package.Id); From 8cdd6a6e0ea2e1fb9696a8140e2a527bc7c4498f Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 10:11:29 +0100 Subject: [PATCH 20/37] Should fix output problem --- package-cleanup/action.yaml | 15 +++ .../ConfigurationExtension.cs | 2 +- .../Service/GiteaPackageService.cs | 95 ++++++++++++------- .../Service/IGiteaPackageService.cs | 1 + .../Service/PackageService.cs | 3 +- sh.actions.package-cleanup/Worker.cs | 72 ++++++++++++-- 6 files changed, 140 insertions(+), 48 deletions(-) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index 68bdc74..a9cbe9f 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -18,6 +18,18 @@ inputs: api_token: description: "API token for authentication" required: true + dry_run: + description: "Execute without deleting packages" + required: false + default: "false" + +outputs: + deleted_packages: + description: "Number of packages deleted" + value: ${{ steps.cleanup.outputs.deleted_packages }} + processed_names: + description: "Number of package names processed" + value: ${{ steps.cleanup.outputs.processed_names }} runs: using: "composite" @@ -31,6 +43,7 @@ runs: chmod +x package-cleanup-linux-x64 - name: Run package-cleanup + id: cleanup shell: bash env: URL: ${{ inputs.url }} @@ -38,5 +51,7 @@ runs: TYPES: ${{ inputs.types }} NAMES: ${{ inputs.names }} API_TOKEN: ${{ inputs.api_token }} + DRY_RUN: ${{ inputs.dry_run }} + GITHUB_OUTPUT: ${{ env.GITHUB_OUTPUT }} run: | ./package-cleanup-linux-x64 diff --git a/sh.actions.package-cleanup/ConfigurationExtension.cs b/sh.actions.package-cleanup/ConfigurationExtension.cs index e248316..c650f7d 100644 --- a/sh.actions.package-cleanup/ConfigurationExtension.cs +++ b/sh.actions.package-cleanup/ConfigurationExtension.cs @@ -15,7 +15,7 @@ public static class ConfigurationExtension public static IConfigurationBuilder EnsureGiteaConfig(this IConfigurationBuilder builder) { var configuration = builder.Build(); - var requiredKeys = new[] { "URL", "OWNER", "TYPES", "NAME", "API_TOKEN" }; + var requiredKeys = new[] { "URL", "OWNER", "TYPES", "NAMES", "API_TOKEN" }; foreach (var key in requiredKeys) { diff --git a/sh.actions.package-cleanup/Service/GiteaPackageService.cs b/sh.actions.package-cleanup/Service/GiteaPackageService.cs index 3a95789..789c8b4 100644 --- a/sh.actions.package-cleanup/Service/GiteaPackageService.cs +++ b/sh.actions.package-cleanup/Service/GiteaPackageService.cs @@ -22,24 +22,75 @@ public class GiteaPackageService( } } - public async Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default) + public async Task> GetPackagesByNameAsync(string name, CancellationToken cancellationToken = default) { try { var baseUrl = GetBaseUrl(); - var allPackages = new List(); + var packages = new List(); // Parse comma-separated types - var types = (configuration["TYPE"] ?? "") + var types = (configuration["TYPES"] ?? "") .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (types.Length == 0) { logger.LogWarning("No package types configured"); - return allPackages; + return packages; } - // Parse comma-separated names + foreach (var type in types) + { + var page = 1; + + while (true) + { + var url = $"{baseUrl}/{type}/{name}?page={page}"; + logger.LogInformation("Fetching packages from Gitea: {Url}", url); + + var request = new HttpRequestMessage(HttpMethod.Get, url); + AddAuthorizationHeader(request); + + var response = await httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + + var fetchedPackages = + await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken) + ?? []; + + if (fetchedPackages.Count == 0) + { + break; + } + + packages.AddRange(fetchedPackages); + logger.LogInformation("Fetched {Count} packages from page {Page}", fetchedPackages.Count, page); + + page++; + } + } + + logger.LogInformation("Successfully fetched {Count} packages for name '{Name}'", packages.Count, name); + return packages; + } + catch (HttpRequestException ex) + { + logger.LogError(ex, "Error fetching packages from Gitea for name '{Name}'", name); + throw; + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected error fetching packages for name '{Name}'", name); + throw; + } + } + + public async Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default) + { + try + { + var allPackages = new List(); + var names = (configuration["NAMES"] ?? "") .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); @@ -49,38 +100,10 @@ public class GiteaPackageService( return allPackages; } - foreach (var type in types) + foreach (var name in names) { - foreach (var name in names) - { - var page = 1; - - while (true) - { - var url = $"{baseUrl}/{type}/{name}?page={page}"; - logger.LogInformation("Fetching packages from Gitea: {Url}", url); - - var request = new HttpRequestMessage(HttpMethod.Get, url); - AddAuthorizationHeader(request); - - var response = await httpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); - - var packages = - await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken) - ?? []; - - if (packages.Count == 0) - { - break; - } - - allPackages.AddRange(packages); - logger.LogInformation("Fetched {Count} packages from page {Page}", packages.Count, page); - - page++; - } - } + var packages = await GetPackagesByNameAsync(name, cancellationToken); + allPackages.AddRange(packages); } logger.LogInformation("Successfully fetched {Count} packages in total", allPackages.Count); diff --git a/sh.actions.package-cleanup/Service/IGiteaPackageService.cs b/sh.actions.package-cleanup/Service/IGiteaPackageService.cs index 7df007e..188ffde 100644 --- a/sh.actions.package-cleanup/Service/IGiteaPackageService.cs +++ b/sh.actions.package-cleanup/Service/IGiteaPackageService.cs @@ -4,6 +4,7 @@ using sh.actions.package_cleanup.Models; public interface IGiteaPackageService { + Task> GetPackagesByNameAsync(string name, CancellationToken cancellationToken = default); Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default); Task DeletePackage(GiteaPackage package, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/sh.actions.package-cleanup/Service/PackageService.cs b/sh.actions.package-cleanup/Service/PackageService.cs index 3a03e9a..6fead71 100644 --- a/sh.actions.package-cleanup/Service/PackageService.cs +++ b/sh.actions.package-cleanup/Service/PackageService.cs @@ -6,8 +6,7 @@ public class PackageService(IGiteaPackageService packageService) : IPackageServi { public async Task> GetFilteredPackages() { - var packages = (await packageService.GetPackagesByOwnerAsync()).ToList(); - return packages; + return (await packageService.GetPackagesByOwnerAsync()).ToList(); } public List FilterPackagesToDelete(List packages) diff --git a/sh.actions.package-cleanup/Worker.cs b/sh.actions.package-cleanup/Worker.cs index 188db1c..659d965 100644 --- a/sh.actions.package-cleanup/Worker.cs +++ b/sh.actions.package-cleanup/Worker.cs @@ -11,20 +11,22 @@ public class Worker( IGiteaPackageService giteaPackageService ) : BackgroundService { - private async Task DeletePackages(List packages, CancellationToken cancellationToken = default) + private async Task DeletePackages(List packages, CancellationToken cancellationToken = default) { var dryRun = configuration["DRY_RUN"]?.ToLower() == "true"; if (dryRun) { logger.LogInformation("Dry run enabled, not deleting {Count} packages", packages.Count); - return; + return 0; } + var deletedCount = 0; foreach (var giteaPackage in packages) { try { await giteaPackageService.DeletePackage(giteaPackage, cancellationToken); + deletedCount++; } catch (Exception ex) { @@ -32,25 +34,77 @@ public class Worker( giteaPackage.Name, giteaPackage.Version); } } + return deletedCount; + } + + private void WriteGitHubOutput(string name, string value) + { + var githubOutput = configuration["GITHUB_OUTPUT"]; + if (string.IsNullOrEmpty(githubOutput)) + { + logger.LogDebug("GITHUB_OUTPUT not set, skipping output: {Name}={Value}", name, value); + return; + } + + try + { + File.AppendAllText(githubOutput, $"{name}={value}{Environment.NewLine}"); + logger.LogInformation("Wrote to GITHUB_OUTPUT: {Name}={Value}", name, value); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to write to GITHUB_OUTPUT file"); + } } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { try { - var packages = await packageService.GetFilteredPackages(); - logger.LogInformation("Found {Count} packages", packages.Count); + // Parse comma-separated names + var names = (configuration["NAMES"] ?? "") + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (names.Length == 0) + { + logger.LogWarning("No package names configured"); + WriteGitHubOutput("deleted_packages", "0"); + WriteGitHubOutput("processed_names", "0"); + appLifetime.StopApplication(); + return; + } + + var totalDeleted = 0; + + // Process each name separately: collect -> filter -> delete + foreach (var name in names) + { + logger.LogInformation("Processing packages for name '{Name}'", name); + + var packages = (await giteaPackageService.GetPackagesByNameAsync(name, cancellationToken)).ToList(); + logger.LogInformation("Found {Count} packages for name '{Name}'", packages.Count, name); + + var packagesToDelete = packageService.FilterPackagesToDelete(packages); + logger.LogInformation("Found {Count} packages to delete for name '{Name}'", packagesToDelete.Count, name); + + var deletedCount = await DeletePackages(packagesToDelete, cancellationToken); + totalDeleted += deletedCount; + logger.LogInformation("Deleted {Count} packages for name '{Name}'", deletedCount, name); + + logger.LogInformation("Cleanup finished for name '{Name}'", name); + } - var packagesToDelete = packageService.FilterPackagesToDelete(packages); - logger.LogInformation("Found {Count} packages to delete", packagesToDelete.Count); + logger.LogInformation("All package names processed successfully"); - await DeletePackages(packagesToDelete, cancellationToken); - - logger.LogInformation("Cleanup finished successfully"); + // Write outputs to GITHUB_OUTPUT + WriteGitHubOutput("deleted_packages", totalDeleted.ToString()); + WriteGitHubOutput("processed_names", names.Length.ToString()); } catch (Exception ex) { logger.LogError(ex, "Cleanup failed with an error"); + WriteGitHubOutput("deleted_packages", "0"); + WriteGitHubOutput("processed_names", "0"); } finally { From a2fe4cb960d68cc75102eab8b4222923d7c3f18b Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 10:17:48 +0100 Subject: [PATCH 21/37] Added debug info [skip ci] --- package-cleanup/action.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index a9cbe9f..255fc95 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -54,4 +54,5 @@ runs: DRY_RUN: ${{ inputs.dry_run }} GITHUB_OUTPUT: ${{ env.GITHUB_OUTPUT }} run: | + echo "Starting cleanup..." ./package-cleanup-linux-x64 From 5dca7e693bdfb58dbf4821f3e31f7db7e5ef7225 Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 10:18:56 +0100 Subject: [PATCH 22/37] Fixed file structure --- .gitea/workflows/build dev.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index 22734e7..e58907b 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -33,14 +33,14 @@ jobs: - name: Build single file executables run: | - cd sh.actions/sh.actions.package-cleanup + cd sh.actions.package-cleanup # Build for Linux x64 dotnet publish -c Release -r linux-x64 -p:Version=$(cat ../../version.txt) -o publish/linux-x64 - name: Upload to Gitea Generic Package Registry run: | - cd sh.actions/sh.actions.package-cleanup + cd sh.actions.package-cleanup curl -X PUT \ -H "Authorization: token ${{ secrets.CI_ACCESS_TOKEN }}" \ -T publish/linux-x64/sh.actions.package-cleanup \ From 6d3a5b193020209de59bcf1f372e3ea9e4c784c4 Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 10:19:44 +0100 Subject: [PATCH 23/37] Fixe build --- .gitea/workflows/build dev.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/build dev.yaml b/.gitea/workflows/build dev.yaml index e58907b..0f1dbb4 100644 --- a/.gitea/workflows/build dev.yaml +++ b/.gitea/workflows/build dev.yaml @@ -36,7 +36,7 @@ jobs: cd sh.actions.package-cleanup # Build for Linux x64 - dotnet publish -c Release -r linux-x64 -p:Version=$(cat ../../version.txt) -o publish/linux-x64 + dotnet publish -c Release -r linux-x64 -p:Version=$(cat ../version.txt) -o publish/linux-x64 - name: Upload to Gitea Generic Package Registry run: | @@ -44,4 +44,4 @@ jobs: curl -X PUT \ -H "Authorization: token ${{ secrets.CI_ACCESS_TOKEN }}" \ -T publish/linux-x64/sh.actions.package-cleanup \ - "https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/$(cat ../../version.txt)/package-cleanup-linux-x64" + "https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/$(cat ../version.txt)/package-cleanup-linux-x64" From 4ff5680310c3533a0e6ac30e0001fe8654d4b0c3 Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 10:29:52 +0100 Subject: [PATCH 24/37] Changed logging --- sh.actions.package-cleanup/Program.cs | 4 ++ sh.actions.package-cleanup/Worker.cs | 56 ++++++--------------------- 2 files changed, 15 insertions(+), 45 deletions(-) diff --git a/sh.actions.package-cleanup/Program.cs b/sh.actions.package-cleanup/Program.cs index 33f69d3..a386024 100644 --- a/sh.actions.package-cleanup/Program.cs +++ b/sh.actions.package-cleanup/Program.cs @@ -15,5 +15,9 @@ builder.Services .AddHostedService() .AddHttpClient(); +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); +builder.Logging.SetMinimumLevel(LogLevel.Debug); + var host = builder.Build(); await host.RunAsync(); diff --git a/sh.actions.package-cleanup/Worker.cs b/sh.actions.package-cleanup/Worker.cs index 659d965..e87ddb3 100644 --- a/sh.actions.package-cleanup/Worker.cs +++ b/sh.actions.package-cleanup/Worker.cs @@ -11,50 +11,26 @@ public class Worker( IGiteaPackageService giteaPackageService ) : BackgroundService { - private async Task DeletePackages(List packages, CancellationToken cancellationToken = default) + private async Task DeletePackages(List packages, CancellationToken cancellationToken = default) { var dryRun = configuration["DRY_RUN"]?.ToLower() == "true"; if (dryRun) { logger.LogInformation("Dry run enabled, not deleting {Count} packages", packages.Count); - return 0; } - var deletedCount = 0; foreach (var giteaPackage in packages) { try { await giteaPackageService.DeletePackage(giteaPackage, cancellationToken); - deletedCount++; } catch (Exception ex) { - logger.LogError(ex, "Failed to delete package {PackageName} version {Version}", + logger.LogError(ex, "Failed to delete package {PackageName} version {Version}", giteaPackage.Name, giteaPackage.Version); } } - return deletedCount; - } - - private void WriteGitHubOutput(string name, string value) - { - var githubOutput = configuration["GITHUB_OUTPUT"]; - if (string.IsNullOrEmpty(githubOutput)) - { - logger.LogDebug("GITHUB_OUTPUT not set, skipping output: {Name}={Value}", name, value); - return; - } - - try - { - File.AppendAllText(githubOutput, $"{name}={value}{Environment.NewLine}"); - logger.LogInformation("Wrote to GITHUB_OUTPUT: {Name}={Value}", name, value); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to write to GITHUB_OUTPUT file"); - } } protected override async Task ExecuteAsync(CancellationToken cancellationToken) @@ -68,43 +44,33 @@ public class Worker( if (names.Length == 0) { logger.LogWarning("No package names configured"); - WriteGitHubOutput("deleted_packages", "0"); - WriteGitHubOutput("processed_names", "0"); appLifetime.StopApplication(); return; } - var totalDeleted = 0; - // Process each name separately: collect -> filter -> delete foreach (var name in names) { logger.LogInformation("Processing packages for name '{Name}'", name); - + var packages = (await giteaPackageService.GetPackagesByNameAsync(name, cancellationToken)).ToList(); logger.LogInformation("Found {Count} packages for name '{Name}'", packages.Count, name); - + var packagesToDelete = packageService.FilterPackagesToDelete(packages); - logger.LogInformation("Found {Count} packages to delete for name '{Name}'", packagesToDelete.Count, name); - - var deletedCount = await DeletePackages(packagesToDelete, cancellationToken); - totalDeleted += deletedCount; - logger.LogInformation("Deleted {Count} packages for name '{Name}'", deletedCount, name); - + logger.LogInformation("Found {Count} packages to delete for name '{Name}'", packagesToDelete.Count, + name); + + await DeletePackages(packagesToDelete, cancellationToken); + logger.LogInformation("Deleted {Count} packages for name '{Name}'", packagesToDelete.Count, name); + logger.LogInformation("Cleanup finished for name '{Name}'", name); } - + logger.LogInformation("All package names processed successfully"); - - // Write outputs to GITHUB_OUTPUT - WriteGitHubOutput("deleted_packages", totalDeleted.ToString()); - WriteGitHubOutput("processed_names", names.Length.ToString()); } catch (Exception ex) { logger.LogError(ex, "Cleanup failed with an error"); - WriteGitHubOutput("deleted_packages", "0"); - WriteGitHubOutput("processed_names", "0"); } finally { From c6fe9f73977173af04026c747ca7d28fb1b9a4dc Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 10:36:03 +0100 Subject: [PATCH 25/37] Added output manually --- package-cleanup/action.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index 255fc95..299e6f6 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -55,4 +55,7 @@ runs: GITHUB_OUTPUT: ${{ env.GITHUB_OUTPUT }} run: | echo "Starting cleanup..." - ./package-cleanup-linux-x64 + ./package-cleanup-linux-x64 > output.txt + cat output.txt + echo "$(cat output.txt)" + echo "Cleanup completed." From 213fc4685d028bc3422dd4a0a692c339b4debf6d Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 11:57:46 +0100 Subject: [PATCH 26/37] Build as single file --- .gitignore | 1 + .../sh.actions.package-cleanup.csproj | 31 +++++++++++++++++++ sh.actions.sln.DotSettings.user | 7 ----- 3 files changed, 32 insertions(+), 7 deletions(-) delete mode 100644 sh.actions.sln.DotSettings.user diff --git a/.gitignore b/.gitignore index dd3c5ba..4fa7b9e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ bin/ obj/ +publish/ # Environment Variables - DO NOT COMMIT .env diff --git a/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj b/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj index 09f4696..8f06e7d 100644 --- a/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj +++ b/sh.actions.package-cleanup/sh.actions.package-cleanup.csproj @@ -6,10 +6,41 @@ enable dotnet-sh.actions.package_cleanup-2b7a013f-ec22-4325-9832-0c9ca9b8ced9 sh.actions.package_cleanup + true + true + + + <_ContentIncludedByDefault Remove="publish\linux-x64\appsettings.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\appsettings.local.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\appsettings.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\appsettings.local.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.local.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.local.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.local.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.local.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.local.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\appsettings.local.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\sh.actions.package-cleanup.deps.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\sh.actions.package-cleanup.runtimeconfig.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\sh.actions.package-cleanup.deps.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\sh.actions.package-cleanup.runtimeconfig.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\sh.actions.package-cleanup.deps.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\publish\linux-x64\sh.actions.package-cleanup.runtimeconfig.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\sh.actions.package-cleanup.deps.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\publish\linux-x64\sh.actions.package-cleanup.runtimeconfig.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\sh.actions.package-cleanup.deps.json" /> + <_ContentIncludedByDefault Remove="publish\linux-x64\sh.actions.package-cleanup.runtimeconfig.json" /> + diff --git a/sh.actions.sln.DotSettings.user b/sh.actions.sln.DotSettings.user deleted file mode 100644 index eb46bbe..0000000 --- a/sh.actions.sln.DotSettings.user +++ /dev/null @@ -1,7 +0,0 @@ - - ForceIncluded - ForceIncluded - <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <Solution /> -</SessionState> - \ No newline at end of file From 79902f323de203ccc7e2ef2208763bb193f5a7b7 Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 12:03:52 +0100 Subject: [PATCH 27/37] Better output --- package-cleanup/action.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index 299e6f6..7a2697c 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -55,7 +55,5 @@ runs: GITHUB_OUTPUT: ${{ env.GITHUB_OUTPUT }} run: | echo "Starting cleanup..." - ./package-cleanup-linux-x64 > output.txt - cat output.txt - echo "$(cat output.txt)" + ./package-cleanup-linux-x64 echo "Cleanup completed." From 17a4e57fa4dfc7c3fb55adac10ef34e9a3932bdf Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 12:07:19 +0100 Subject: [PATCH 28/37] Added test output --- package-cleanup/action.yaml | 8 -------- sh.actions.package-cleanup/Program.cs | 1 + sh.actions.package-cleanup/Worker.cs | 8 +++++++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index 7a2697c..bb999fc 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -23,14 +23,6 @@ inputs: required: false default: "false" -outputs: - deleted_packages: - description: "Number of packages deleted" - value: ${{ steps.cleanup.outputs.deleted_packages }} - processed_names: - description: "Number of package names processed" - value: ${{ steps.cleanup.outputs.processed_names }} - runs: using: "composite" steps: diff --git a/sh.actions.package-cleanup/Program.cs b/sh.actions.package-cleanup/Program.cs index a386024..00c88c2 100644 --- a/sh.actions.package-cleanup/Program.cs +++ b/sh.actions.package-cleanup/Program.cs @@ -20,4 +20,5 @@ builder.Logging.AddConsole(); builder.Logging.SetMinimumLevel(LogLevel.Debug); var host = builder.Build(); +Console.WriteLine("STARTING"); await host.RunAsync(); diff --git a/sh.actions.package-cleanup/Worker.cs b/sh.actions.package-cleanup/Worker.cs index e87ddb3..1486908 100644 --- a/sh.actions.package-cleanup/Worker.cs +++ b/sh.actions.package-cleanup/Worker.cs @@ -60,7 +60,13 @@ public class Worker( logger.LogInformation("Found {Count} packages to delete for name '{Name}'", packagesToDelete.Count, name); - await DeletePackages(packagesToDelete, cancellationToken); + Console.WriteLine($"Found {packagesToDelete.Count} packages to delete for name '{name}':"); + foreach (var pkg in packagesToDelete) + { + Console.WriteLine($"- {pkg.Name} {pkg.Version}"); + } + + // await DeletePackages(packagesToDelete, cancellationToken); logger.LogInformation("Deleted {Count} packages for name '{Name}'", packagesToDelete.Count, name); logger.LogInformation("Cleanup finished for name '{Name}'", name); From 3ba2b71f4a8b922cd6a687b8a1c174a4aeb60900 Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 12:08:45 +0100 Subject: [PATCH 29/37] Hold to debug --- package-cleanup/action.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index bb999fc..ccdd640 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -47,5 +47,6 @@ runs: GITHUB_OUTPUT: ${{ env.GITHUB_OUTPUT }} run: | echo "Starting cleanup..." - ./package-cleanup-linux-x64 + ./package-cleanup-linux-x64 > output.txt echo "Cleanup completed." + sleep 100000000 From c1a882d1a213f49a106d17675ce7fd38f6db690c Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 12:12:52 +0100 Subject: [PATCH 30/37] Cleanup to ensure repull --- package-cleanup/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index ccdd640..935dd17 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -47,6 +47,6 @@ runs: GITHUB_OUTPUT: ${{ env.GITHUB_OUTPUT }} run: | echo "Starting cleanup..." - ./package-cleanup-linux-x64 > output.txt + ./package-cleanup-linux-x64 echo "Cleanup completed." - sleep 100000000 + rm package-cleanup-linux-x64 From 4497b67b702d56d4a46907520a5d7cfb37973315 Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 12:17:45 +0100 Subject: [PATCH 31/37] force dry run via start arg --- package-cleanup/action.yaml | 2 ++ sh.actions.package-cleanup/Program.cs | 8 +++++++- sh.actions.package-cleanup/Worker.cs | 12 ++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index 935dd17..d276c0e 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -49,4 +49,6 @@ runs: echo "Starting cleanup..." ./package-cleanup-linux-x64 echo "Cleanup completed." + sleep 10000000 rm package-cleanup-linux-x64 + diff --git a/sh.actions.package-cleanup/Program.cs b/sh.actions.package-cleanup/Program.cs index 00c88c2..a6753f8 100644 --- a/sh.actions.package-cleanup/Program.cs +++ b/sh.actions.package-cleanup/Program.cs @@ -3,6 +3,13 @@ using sh.actions.package_cleanup.Service; var builder = Host.CreateApplicationBuilder(args); +// Check for --dry-run argument +var isDryRun = args.Contains("--dry-run"); +if (isDryRun) +{ + Environment.SetEnvironmentVariable("DRY_RUN", "true"); +} + builder.Configuration .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true) @@ -20,5 +27,4 @@ builder.Logging.AddConsole(); builder.Logging.SetMinimumLevel(LogLevel.Debug); var host = builder.Build(); -Console.WriteLine("STARTING"); await host.RunAsync(); diff --git a/sh.actions.package-cleanup/Worker.cs b/sh.actions.package-cleanup/Worker.cs index 1486908..5df0457 100644 --- a/sh.actions.package-cleanup/Worker.cs +++ b/sh.actions.package-cleanup/Worker.cs @@ -37,6 +37,12 @@ public class Worker( { try { + var dryRun = configuration["DRY_RUN"]?.ToLower() == "true"; + if (dryRun) + { + logger.LogInformation("DRY RUN MODE ENABLED - No packages will be deleted"); + } + // Parse comma-separated names var names = (configuration["NAMES"] ?? "") .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); @@ -60,12 +66,6 @@ public class Worker( logger.LogInformation("Found {Count} packages to delete for name '{Name}'", packagesToDelete.Count, name); - Console.WriteLine($"Found {packagesToDelete.Count} packages to delete for name '{name}':"); - foreach (var pkg in packagesToDelete) - { - Console.WriteLine($"- {pkg.Name} {pkg.Version}"); - } - // await DeletePackages(packagesToDelete, cancellationToken); logger.LogInformation("Deleted {Count} packages for name '{Name}'", packagesToDelete.Count, name); From a2e4152d5edb5c20e13179f216d0955c794ac616 Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 12:24:00 +0100 Subject: [PATCH 32/37] Should fix exec problems --- package-cleanup/action.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index d276c0e..e3518ef 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -29,7 +29,7 @@ runs: - name: Download and test package-cleanup tool shell: bash run: | - curl -OJ https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/package-cleanup-linux-x64 + curl -OJ https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/latest/package-cleanup-linux-x64 # Make executable chmod +x package-cleanup-linux-x64 @@ -49,6 +49,4 @@ runs: echo "Starting cleanup..." ./package-cleanup-linux-x64 echo "Cleanup completed." - sleep 10000000 - rm package-cleanup-linux-x64 From 706d9f89f06c8fd5d1bc0cf726939408a54cfc49 Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 12:42:40 +0100 Subject: [PATCH 33/37] Fixed download url of cleanup tool --- package-cleanup/action.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index e3518ef..08ee387 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -29,7 +29,13 @@ runs: - name: Download and test package-cleanup tool shell: bash run: | - curl -OJ https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/latest/package-cleanup-linux-x64 + latest_path=$(curl -sI https://git.sh-edraft.de/sh-edraft.de/-/packages/generic/package-cleanup/ \ + | awk -F ' ' '/Location:/ {print $2}' \ + | tr -d '\r') + + version=$(basename "$latest_path") + echo "Downloading package-cleanup version $version..." + curl -OJ https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/$version/package-cleanup-linux-x64 # Make executable chmod +x package-cleanup-linux-x64 From 7e4abe9efd4fa56f4c6b0ccd28ce828712200c22 Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 12:46:24 +0100 Subject: [PATCH 34/37] Readded delete function --- package-cleanup/action.yaml | 2 -- sh.actions.package-cleanup/Worker.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index 08ee387..147ca58 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -52,7 +52,5 @@ runs: DRY_RUN: ${{ inputs.dry_run }} GITHUB_OUTPUT: ${{ env.GITHUB_OUTPUT }} run: | - echo "Starting cleanup..." ./package-cleanup-linux-x64 - echo "Cleanup completed." diff --git a/sh.actions.package-cleanup/Worker.cs b/sh.actions.package-cleanup/Worker.cs index 5df0457..cdc9631 100644 --- a/sh.actions.package-cleanup/Worker.cs +++ b/sh.actions.package-cleanup/Worker.cs @@ -66,7 +66,7 @@ public class Worker( logger.LogInformation("Found {Count} packages to delete for name '{Name}'", packagesToDelete.Count, name); - // await DeletePackages(packagesToDelete, cancellationToken); + await DeletePackages(packagesToDelete, cancellationToken); logger.LogInformation("Deleted {Count} packages for name '{Name}'", packagesToDelete.Count, name); logger.LogInformation("Cleanup finished for name '{Name}'", name); From ff8cd3350b02dbefeacaa631385006f647cca34b Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 15 Feb 2026 12:49:58 +0100 Subject: [PATCH 35/37] Added more debugging --- sh.actions.package-cleanup/Worker.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sh.actions.package-cleanup/Worker.cs b/sh.actions.package-cleanup/Worker.cs index cdc9631..38dcc57 100644 --- a/sh.actions.package-cleanup/Worker.cs +++ b/sh.actions.package-cleanup/Worker.cs @@ -17,6 +17,8 @@ public class Worker( if (dryRun) { logger.LogInformation("Dry run enabled, not deleting {Count} packages", packages.Count); + logger.LogInformation("Would delete packages: {versions}", + string.Join(", ", packages.Select(p => p.Version))); } foreach (var giteaPackage in packages) From 4e8b04da7b5f08ca43d0a626dd0127c53244899c Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 22 Feb 2026 00:17:23 +0100 Subject: [PATCH 36/37] Fixed error handling --- package-cleanup/action.yaml | 2 +- .../Service/PackageServiceTests.cs | 48 ++++++++++++++++++- sh.actions.package-cleanup/Worker.cs | 27 +++++++---- sh.actions.sln.DotSettings.user | 4 ++ 4 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 sh.actions.sln.DotSettings.user diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index 147ca58..d35717e 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -14,7 +14,7 @@ inputs: types: description: "Types of packages (e.g. Container, PyPi, NuGet)" required: false - default: "Container,PyPi,NuGet" + default: "container,pypi,nuget,npm" api_token: description: "API token for authentication" required: true diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceTests.cs index c2bbec0..5e2490e 100644 --- a/sh.actions.package-cleanup.Tests/Service/PackageServiceTests.cs +++ b/sh.actions.package-cleanup.Tests/Service/PackageServiceTests.cs @@ -22,6 +22,29 @@ public class PackageFilterTests }).ToList(); } + [Fact] + public void TestPackageNameParsing() + { + var inputString = + "@sh-edraft.de/core, sh-edraft.core.api, sh-edraft.core.api.auth, sh-edraft.core.api.configuration, sh-edraft.core.api.db, sh-edraft.core.api.graphql, sh-edraft.core.api.service, sh-edraft.core.utils"; + List expected = [ + "@sh-edraft.de/core", + "sh-edraft.core.api", + "sh-edraft.core.api.auth", + "sh-edraft.core.api.configuration", + "sh-edraft.core.api.db", + "sh-edraft.core.api.graphql", + "sh-edraft.core.api.service", + "sh-edraft.core.utils" + ]; + + var actual = inputString + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + Assert.Equal(expected.Count, actual.Length); + Assert.Equal(expected, actual); + } + [Fact] public void TestFilterPackagesToDelete() { @@ -102,7 +125,26 @@ public class PackageFilterTests "2024.8.10.0-exp", "2024.8.10.1-exp", "2024.8.11.0-exp", - "2024.8.11.2-exp" + "2024.8.11.2-exp", + "2026.2.21.27-dev", + "2026.2.21.26-dev", + "2026.2.21.25-dev", + "2026.2.21.24-dev", + "2026.2.21.23-dev", + "2026.2.21.22-dev", + "2026.2.21.21-dev", + "2026.2.21.20-dev", + "2026.2.21.19-dev", + "2026.2.21.18-dev", + "2026.2.21.17-dev", + "2026.2.21.15-dev", + "2026.2.21.14-dev", + "2026.2.21.13-dev", + "2026.2.21.12-dev", + "2026.2.21.11-dev", + "2026.2.21.10-dev", + "2026.2.21.9-dev", + "2026.2.21.8-dev", ]; private readonly List _versionsToHold = @@ -139,7 +181,9 @@ public class PackageFilterTests "0.1.1-exp", "0.1.2-exp", "2024.8.11.0-exp", - "2024.8.11.2-exp" + "2024.8.11.2-exp", + "2026.2.21.27-dev", + "2026.2.21.26-dev", ]; private List _expectDeleted => _versions.Except(_versionsToHold).ToList(); diff --git a/sh.actions.package-cleanup/Worker.cs b/sh.actions.package-cleanup/Worker.cs index 38dcc57..2f5b937 100644 --- a/sh.actions.package-cleanup/Worker.cs +++ b/sh.actions.package-cleanup/Worker.cs @@ -56,22 +56,31 @@ public class Worker( return; } + logger.LogInformation("Deleting {count} packages: {names}", names.Length, string.Join(", ", names)); + // Process each name separately: collect -> filter -> delete foreach (var name in names) { - logger.LogInformation("Processing packages for name '{Name}'", name); + try + { + logger.LogInformation("Processing packages for name '{Name}'", name); - var packages = (await giteaPackageService.GetPackagesByNameAsync(name, cancellationToken)).ToList(); - logger.LogInformation("Found {Count} packages for name '{Name}'", packages.Count, name); + var packages = (await giteaPackageService.GetPackagesByNameAsync(name, cancellationToken)).ToList(); + logger.LogInformation("Found {Count} packages for name '{Name}'", packages.Count, name); - var packagesToDelete = packageService.FilterPackagesToDelete(packages); - logger.LogInformation("Found {Count} packages to delete for name '{Name}'", packagesToDelete.Count, - name); + var packagesToDelete = packageService.FilterPackagesToDelete(packages); + logger.LogInformation("Found {Count} packages to delete for name '{Name}'", packagesToDelete.Count, + name); - await DeletePackages(packagesToDelete, cancellationToken); - logger.LogInformation("Deleted {Count} packages for name '{Name}'", packagesToDelete.Count, name); + await DeletePackages(packagesToDelete, cancellationToken); + logger.LogInformation("Deleted {Count} packages for name '{Name}'", packagesToDelete.Count, name); - logger.LogInformation("Cleanup finished for name '{Name}'", name); + logger.LogInformation("Cleanup finished for name '{Name}'", name); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to delete package {PackageName}", name); + } } logger.LogInformation("All package names processed successfully"); diff --git a/sh.actions.sln.DotSettings.user b/sh.actions.sln.DotSettings.user new file mode 100644 index 0000000..be8a2cb --- /dev/null +++ b/sh.actions.sln.DotSettings.user @@ -0,0 +1,4 @@ + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;sh.actions.package-cleanup.Tests&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <Project Location="/home/sven/dev/git_sh-edraft_de/actions/sh.actions.package-cleanup.Tests" Presentation="&lt;sh.actions.package-cleanup.Tests&gt;" /> +</SessionState> \ No newline at end of file From 321e53be5b2c2628cebcda2ddbaee7d9fe4d60b9 Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 22 Feb 2026 00:18:35 +0100 Subject: [PATCH 37/37] Added prod build [skip-ci] --- .gitea/workflows/build.yaml | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .gitea/workflows/build.yaml diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..4efcc23 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,45 @@ +name: Build on push +run-name: Build on push +on: + push: + branches: + - master + +jobs: + prepare: + runs-on: [runner] + container: git.sh-edraft.de/sh-edraft.de/act-runner:latest + steps: + - uses: https://git.sh-edraft.de/sh-edraft.de/actions/set-version@master + env: + CI_ACCESS_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }} + + build: + runs-on: [runner] + needs: prepare + container: git.sh-edraft.de/sh-edraft.de/act-runner:latest + steps: + - name: Clone Repository + uses: https://github.com/actions/checkout@v3 + with: + token: ${{ secrets.CI_ACCESS_TOKEN }} + + - name: Download build version artifact + uses: actions/download-artifact@v3 + with: + name: version + + - name: Build single file executables + run: | + cd sh.actions.package-cleanup + + # Build for Linux x64 + dotnet publish -c Release -r linux-x64 -p:Version=$(cat ../version.txt) -o publish/linux-x64 + + - name: Upload to Gitea Generic Package Registry + run: | + cd sh.actions.package-cleanup + curl -X PUT \ + -H "Authorization: token ${{ secrets.CI_ACCESS_TOKEN }}" \ + -T publish/linux-x64/sh.actions.package-cleanup \ + "https://git.sh-edraft.de/api/packages/sh-edraft.de/generic/package-cleanup/$(cat ../version.txt)/package-cleanup-linux-x64"