diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f7d6b1..8c01b39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,27 +1,113 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build-and-test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Build - run: dotnet build -c Release - - - name: Test - run: dotnet test --logger "console;verbosity=normal" - - - name: Check formatting - run: dotnet format FlowForge.sln --verify-no-changes +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +# Cancel superseded runs on the same ref so stale builds don't pile up. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Least privilege: CI only needs to read the repo. +permissions: + contents: read + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + format: + name: format + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Build.props') }} + restore-keys: ${{ runner.os }}-nuget- + - run: dotnet restore + - name: Verify formatting + run: dotnet format FlowForge.sln --verify-no-changes + + # Job name kept as "build-and-test" so the existing required status check + # keeps being reported with zero branch-protection changes. + build-and-test: + name: build-and-test + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Build.props') }} + restore-keys: ${{ runner.os }}-nuget- + - run: dotnet restore + - name: Build (Release) + run: dotnet build -c Release --no-restore + - name: Test (Release, no rebuild) + run: dotnet test -c Release --no-build --logger "console;verbosity=normal" + + # Mirror of build-and-test on the actual release target (win-x64). + build-and-test-windows: + name: build-and-test-windows + runs-on: windows-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Build.props') }} + restore-keys: ${{ runner.os }}-nuget- + - run: dotnet restore + - name: Build (Release) + run: dotnet build -c Release --no-restore + - name: Test (Release, no rebuild) + run: dotnet test -c Release --no-build --logger "console;verbosity=normal" + + audit: + name: dependency-audit + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - run: dotnet restore + - name: Report deprecated packages (informational) + run: dotnet list package --deprecated --include-transitive || true + - name: Fail on unaccepted vulnerable packages + shell: bash + run: | + dotnet list package --vulnerable --include-transitive 2>&1 | tee vuln.txt + # Vulnerable entries are printed as table rows beginning with "> ". + # Allowlist: Tmds.DBus.Protocol — transitive via Avalonia.FreeDesktop, + # Linux-only, not exploitable on the win-x64 target. Mirrors the + # NuGetAuditSuppress entry in Directory.Build.props. + if grep -E '^\s*>\s' vuln.txt | grep -viq 'Tmds\.DBus\.Protocol'; then + echo "::error::Unaccepted vulnerable package(s) detected:" + grep -E '^\s*>\s' vuln.txt | grep -vi 'Tmds\.DBus\.Protocol' + exit 1 + fi + echo "No unaccepted vulnerable packages." diff --git a/tests/FlowForge.Tests/Nodes/RenameAddAffixNodeTests.cs b/tests/FlowForge.Tests/Nodes/RenameAddAffixNodeTests.cs index 60231ff..eced35e 100644 --- a/tests/FlowForge.Tests/Nodes/RenameAddAffixNodeTests.cs +++ b/tests/FlowForge.Tests/Nodes/RenameAddAffixNodeTests.cs @@ -95,12 +95,13 @@ public async Task DryRun_updates_path_without_file_move() var node = new RenameAddAffixNode(NullLogger.Instance); node.Configure(MakeConfig(new { prefix = "NEW_" })); - FileJob job = MakeJob(Path.Combine("/nonexistent/path", "test.jpg")); + string inputPath = Path.Combine("/nonexistent/path", "test.jpg"); + FileJob job = MakeJob(inputPath); IEnumerable result = await node.TransformAsync(job, dryRun: true); FileJob output = result.Single(); output.FileName.Should().Be("NEW_test.jpg"); - output.CurrentPath.Should().Be(Path.Combine("/nonexistent/path", "NEW_test.jpg")); + output.CurrentPath.Should().Be(Path.Combine(Path.GetDirectoryName(inputPath)!, "NEW_test.jpg")); output.NodeLog.Should().ContainSingle() .Which.Should().Contain("RenameAddAffix:"); } diff --git a/tests/FlowForge.Tests/Nodes/RenameRegexNodeTests.cs b/tests/FlowForge.Tests/Nodes/RenameRegexNodeTests.cs index d25316d..1a2dc80 100644 --- a/tests/FlowForge.Tests/Nodes/RenameRegexNodeTests.cs +++ b/tests/FlowForge.Tests/Nodes/RenameRegexNodeTests.cs @@ -172,12 +172,13 @@ public async Task DryRun_updates_CurrentPath_without_calling_File_Move() var node = new RenameRegexNode(NullLogger.Instance); node.Configure(MakeConfig(new { pattern = @"\d+", replacement = "X" })); - FileJob job = MakeJob(Path.Combine("/nonexistent/path", "file99.txt")); + string inputPath = Path.Combine("/nonexistent/path", "file99.txt"); + FileJob job = MakeJob(inputPath); IEnumerable result = await node.TransformAsync(job, dryRun: true); FileJob output = result.Single(); output.FileName.Should().Be("fileX.txt"); - output.CurrentPath.Should().Be(Path.Combine("/nonexistent/path", "fileX.txt")); + output.CurrentPath.Should().Be(Path.Combine(Path.GetDirectoryName(inputPath)!, "fileX.txt")); output.NodeLog.Should().ContainSingle() .Which.Should().Contain("RenameRegex:"); } diff --git a/tests/FlowForge.Tests/Settings/AppSettingsManagerTests.cs b/tests/FlowForge.Tests/Settings/AppSettingsManagerTests.cs index 44c7433..29382c3 100644 --- a/tests/FlowForge.Tests/Settings/AppSettingsManagerTests.cs +++ b/tests/FlowForge.Tests/Settings/AppSettingsManagerTests.cs @@ -35,7 +35,7 @@ public async Task Save_then_load_roundtrip_preserves_all_properties() DefaultInputFolder = "/home/user/input", DefaultOutputFolder = "/home/user/output", MaxConcurrency = 4, - RecentPipelines = new List { "/home/user/pipelines/test.ffpipe" }, + RecentPipelines = new List { Path.Combine(dir.Path, "pipelines", "test.ffpipe") }, }; await manager.SaveAsync(original);