Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/WriteSpreadsheet_release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: WriteSpreadsheet_release
permissions:
contents: write

on:
workflow_dispatch:

jobs:
build:
uses: FrendsPlatform/FrendsTasks/.github/workflows/release.yml@main

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the workflow files exist and read the relevant lines
cd .github/workflows

echo "=== WriteSpreadsheet_release.yml ===" 
if [ -f WriteSpreadsheet_release.yml ]; then
  sed -n '8,12p' WriteSpreadsheet_release.yml
else
  echo "File not found"
fi

echo ""
echo "=== WriteSpreadsheet_test_on_main.yml ===" 
if [ -f WriteSpreadsheet_test_on_main.yml ]; then
  sed -n '13,17p' WriteSpreadsheet_test_on_main.yml
else
  echo "File not found"
fi

echo ""
echo "=== WriteSpreadsheet_test_on_push.yml ===" 
if [ -f WriteSpreadsheet_test_on_push.yml ]; then
  sed -n '13,17p' WriteSpreadsheet_test_on_push.yml
else
  echo "File not found"
fi

Repository: FrendsPlatform/Frends.Odf

Length of output: 630


Pin all three reusable-workflow refs to immutable commit SHAs instead of mutable @main branches. Mutable refs allow upstream changes to affect your workflows without awareness, creating a supply-chain risk.

  • .github/workflows/WriteSpreadsheet_release.yml#L10: replace release.yml@main with release.yml@<commit_sha>
  • .github/workflows/WriteSpreadsheet_test_on_main.yml#L15: replace linux_build_main.yml@main with linux_build_main.yml@<commit_sha>
  • .github/workflows/WriteSpreadsheet_test_on_push.yml#L15: replace linux_build_test.yml@main with linux_build_test.yml@<commit_sha>
🧰 Tools
🪛 zizmor (1.25.2)

[error] 10-10: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

📍 Affects 3 files
  • .github/workflows/WriteSpreadsheet_release.yml#L10-L10 (this comment)
  • .github/workflows/WriteSpreadsheet_test_on_main.yml#L15-L15
  • .github/workflows/WriteSpreadsheet_test_on_push.yml#L15-L15
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/WriteSpreadsheet_release.yml at line 10, Replace all
mutable `@main` branch references with immutable commit SHAs in the reusable
workflow calls to mitigate supply-chain risk. In
.github/workflows/WriteSpreadsheet_release.yml at line 10, replace the ref
`@main` in the uses statement for release.yml with a specific commit SHA. In
.github/workflows/WriteSpreadsheet_test_on_main.yml at line 15, replace the ref
`@main` in the uses statement for linux_build_main.yml with a specific commit
SHA. In .github/workflows/WriteSpreadsheet_test_on_push.yml at line 15, replace
the ref `@main` in the uses statement for linux_build_test.yml with a specific
commit SHA.

Source: Linters/SAST tools

with:
workdir: Frends.Odf.WriteSpreadsheet
dotnet_version: 8.0.x
strict_analyzers: true
secrets:
feed_api_key: ${{ secrets.TASKS_FEED_API_KEY }}
21 changes: 21 additions & 0 deletions .github/workflows/WriteSpreadsheet_test_on_main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: WriteSpreadsheet_test_on_main
permissions:
contents: read

on:
push:
branches:
- main
paths:
- 'Frends.Odf.WriteSpreadsheet/**'
workflow_dispatch:

jobs:
build:
uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_main.yml@main
with:
workdir: Frends.Odf.WriteSpreadsheet
dotnet_version: 8.0.x
strict_analyzers: true
secrets:
badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }}
22 changes: 22 additions & 0 deletions .github/workflows/WriteSpreadsheet_test_on_push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: WriteSpreadsheet_test_on_push
permissions:
contents: read

on:
push:
branches-ignore:
- main
paths:
- 'Frends.Odf.WriteSpreadsheet/**'
workflow_dispatch:

jobs:
build:
uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_test.yml@main
with:
workdir: Frends.Odf.WriteSpreadsheet
dotnet_version: 8.0.x
strict_analyzers: true
secrets:
badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }}
test_feed_api_key: ${{ secrets.TASKS_TEST_FEED_API_KEY }}
7 changes: 7 additions & 0 deletions Frends.Odf.WriteSpreadsheet/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Changelog

## [1.0.0] - 2026-06-08

### Added

- Initial implementation
Comment on lines +3 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Expand the 1.0.0 entry with concrete functional changes.

The current note (“Initial implementation”) is too generic. Please list the shipped behavior (e.g., creating .ods from JSON payload, overwrite/throw behavior, header options), and add upgrade notes if any future release introduces breaking changes.

As per coding guidelines, “Validate format against Keep a Changelog … Include all functional changes and indicate breaking changes with upgrade notes.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Frends.Odf.WriteSpreadsheet/CHANGELOG.md` around lines 3 - 7, The 1.0.0
changelog entry in the CHANGELOG.md file is too generic with only "Initial
implementation" listed. Replace this vague description with specific functional
changes that were actually shipped, such as the ability to create .ods
spreadsheet files from JSON payload, details about overwrite vs. throw behavior
when files already exist, header row options, and any other concrete features.
Ensure the description follows the Keep a Changelog format and add upgrade notes
indicating any breaking changes (relevant for future releases). Make the entry
detailed enough that users understand exactly what functionality is available in
version 1.0.0.

Source: Coding guidelines

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.IO;
using System.Threading;
using NUnit.Framework;

namespace Frends.Odf.WriteSpreadsheet.Tests;

[TestFixture]
internal class ErrorHandlerTest : TestBase
{
private const string CustomErrorMessage = "CustomErrorMessage";

// Fake path to trigger DirectoryNotFoundException.
private readonly string fakePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "fake_path.ods");

[Test]
public void Should_Throw_Error_When_ThrowErrorOnFailure_Is_True()
{
var input = DefaultInput();
input.FilePath = fakePath;

var ex = Assert.Throws<Exception>(() =>
Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None));

Assert.That(ex, Is.Not.Null);
}

[Test]
public void Should_Return_Failed_Result_When_ThrowErrorOnFailure_Is_False()
{
var input = DefaultInput();
input.FilePath = fakePath;

var options = DefaultOptions();
options.ThrowErrorOnFailure = false;

var result = Odf.WriteSpreadsheet(input, options, CancellationToken.None);

Assert.That(result.Success, Is.False);
Assert.That(result.Error, Is.Not.Null);
}

[Test]
public void Should_Use_Custom_ErrorMessageOnFailure()
{
var input = DefaultInput();
input.FilePath = fakePath;

var options = DefaultOptions();
options.ErrorMessageOnFailure = CustomErrorMessage;

var ex = Assert.Throws<Exception>(() =>
Odf.WriteSpreadsheet(input, options, CancellationToken.None));

Assert.That(ex, Is.Not.Null);
Assert.That(ex.Message, Contains.Substring(CustomErrorMessage));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>disable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Frends.Odf.WriteSpreadsheet\Frends.Odf.WriteSpreadsheet.csproj" />
</ItemGroup>

<ItemGroup>
<None Include=".env" CopyToOutputDirectory="PreserveNewest" Condition="Exists('.env')" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageReference Include="coverlet.collector" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.*" />
<PackageReference Include="NUnit" Version="4.*" />
<PackageReference Include="NUnit3TestAdapter" Version="6.*" />
<PackageReference Include="dotenv.net" Version="4.*" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
using System;
using System.IO;
using System.Threading;
using Frends.Odf.WriteSpreadsheet.Definitions;
using Frends.Odf.WriteSpreadsheet.Tests.Helpers;
using NUnit.Framework;

namespace Frends.Odf.WriteSpreadsheet.Tests;

[TestFixture]
internal class FunctionalTests : TestBase
{
[Test]
public void Should_Write_Input_Data()
{
var input = DefaultInput();
var options = DefaultOptions();

var result = Odf.WriteSpreadsheet(input, options, CancellationToken.None);

Assert.That(result.Success, Is.True, "Task failed to execute successfully.");
Assert.That(File.Exists(result.FilePath), Is.True, "The output .ods file was not created.");

var xmlString = TestHelper.ReadOdsContent(result.FilePath);

Assert.That(xmlString, Contains.Substring("John"));
Assert.That(xmlString, Contains.Substring("Test 1"));
Assert.That(xmlString, Contains.Substring("Doe"));
Assert.That(xmlString, Contains.Substring("Test 2"));
}

[Test]
public void Should_Generate_Headers_If_IncludeHeaderRow_Is_True()
{
var input = DefaultInput();
var options = DefaultOptions();
options.IncludeHeaderRow = true;

var result = Odf.WriteSpreadsheet(input, options, CancellationToken.None);

Assert.That(result.Success, Is.True);
var xmlString = TestHelper.ReadOdsContent(result.FilePath);

Assert.That(xmlString, Contains.Substring("Name"));
Assert.That(xmlString, Contains.Substring("Test"));
}

[Test]
public void Should_Throw_When_Input_FilePath_Is_Incorrect()
{
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "fake_path.ods");

var input = DefaultInput();
input.FilePath = path;

var exception = Assert.Throws<Exception>(() => Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None));

Assert.That(exception.Message, Contains.Substring("Destination directory not found"));
}

[Test]
public void Should_Throw_When_Input_Data_Is_Incorrect()
{
var invalidPayload = @"{ ""Name"": ""John"" }";

var input = DefaultInput();
input.Payload = invalidPayload;

var exception = Assert.Throws<Exception>(() => Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None));

Assert.That(exception.Message, Contains.Substring("must be a valid array of objects"));
}

[Test]
public void Should_Throw_When_ActionOnExistingFile_Is_Throw()
{
File.WriteAllText(ValidTestFilePath, "This is an existing file.");

var input = DefaultInput();
input.ActionOnExistingFile = ActionOnExistingFile.Throw;

var exception = Assert.Throws<Exception>(() => Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None));

Assert.That(exception.Message, Contains.Substring("File already exists"));
}

[Test]
public void Should_Overwrite_When_ActionOnExistingFile_Is_Overwrite()
{
File.WriteAllText(ValidTestFilePath, "This is an existing file.");

var input = DefaultInput();
input.ActionOnExistingFile = ActionOnExistingFile.Overwrite;

var result = Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None);

Assert.That(result.Success, Is.True);
Assert.That(File.Exists(result.FilePath), Is.True);

var xmlString = TestHelper.ReadOdsContent(result.FilePath);

Assert.That(xmlString, Contains.Substring("John"));
Assert.That(xmlString, Does.Not.Contain("This is an existing file."));
}

[Test]
public void Should_Write_Empty_Spreadsheet_With_Empty_Payload()
{
var emptyPayload = "[]";

var input = DefaultInput();
input.Payload = emptyPayload;

var options = DefaultOptions();
options.IncludeHeaderRow = false;

var result = Odf.WriteSpreadsheet(input, options, CancellationToken.None);

Assert.That(result.Success, Is.True);
Assert.That(File.Exists(result.FilePath), Is.True);

var xmlString = TestHelper.ReadOdsContent(result.FilePath);

Assert.That(xmlString, Does.Not.Contain("John"));
Assert.That(xmlString, Does.Not.Contain("<table:table-cell"));
}

[Test]
public void Should_Handle_Unicode_Content()
{
var unicodePayload = @"[
{ ""Text1"": ""AäÄaOöÖo."" },
{ ""Text2"": ""ÖöÄä."" }
]";

var input = DefaultInput();
input.Payload = unicodePayload;

var result = Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None);

Assert.That(result.Success, Is.True);
var xmlString = TestHelper.ReadOdsContent(result.FilePath);

Assert.That(xmlString, Contains.Substring("AäÄaOöÖo."));
Assert.That(xmlString, Contains.Substring("ÖöÄä."));
}

[Test]
public void Should_Store_Formula_Like_Values_As_Strings()
{
var formulaPayload = @"[
{ ""Equals"": ""=SUM(A1:A2)"", ""Plus"": ""+100"", ""Minus"": ""-50"", ""At"": ""@Test"" }
]";

var input = DefaultInput();
input.Payload = formulaPayload;

var result = Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None);

Assert.That(result.Success, Is.True);
var xmlString = TestHelper.ReadOdsContent(result.FilePath);

Assert.That(xmlString, Contains.Substring("=SUM(A1:A2)"));
Assert.That(xmlString, Contains.Substring("+100"));
Assert.That(xmlString, Contains.Substring("-50"));
Assert.That(xmlString, Contains.Substring("@Test"));
}

[Test]
public void Should_Handle_Partially_Empty_Json()
{
var partialPayload = @"[
{ ""Col1"": ""Row1"" },
{ ""Col2"": ""Row2"" }
]";

var input = DefaultInput();
input.Payload = partialPayload;

var result = Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None);

Assert.That(result.Success, Is.True);
var xmlString = TestHelper.ReadOdsContent(result.FilePath);

Assert.That(xmlString, Contains.Substring("Col1"));
Assert.That(xmlString, Contains.Substring("Col2"));
Assert.That(xmlString, Contains.Substring("Row1"));
Assert.That(xmlString, Contains.Substring("Row2"));
}

[Test]
public void Should_Write_Correct_Value_Types()
{
var typedPayload = @"[
{ ""StringCol"": ""Text"", ""NumCol"": 42.5, ""BoolCol"": true, ""DateCol"": ""2026-06-12T12:00:00"" }
]";

var input = DefaultInput();
input.Payload = typedPayload;

var result = Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None);

Assert.That(result.Success, Is.True);
var xmlString = TestHelper.ReadOdsContent(result.FilePath);

Assert.That(xmlString, Contains.Substring("office:value-type=\"string\""));
Assert.That(xmlString, Contains.Substring("office:value-type=\"float\""));
Assert.That(xmlString, Contains.Substring("office:value=\"42.5\""));
Assert.That(xmlString, Contains.Substring("office:value-type=\"boolean\""));
Assert.That(xmlString, Contains.Substring("office:boolean-value=\"true\""));
Assert.That(xmlString, Contains.Substring("office:value-type=\"date\""));
Assert.That(xmlString, Contains.Substring("office:date-value=\"2026-06-12T12:00:00\""));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("StyleCop.CSharp.SpecialRules", "SA0001::XmlCommentAnalysisDisabled", Justification = "Following Frends documentation guidelines")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:PrefixLocalCallsWithThis", Justification = "Following Frends documentation guidelines")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:UsingDirectivesMustBePlacedWithinNamespace", Justification = "Following Frends documentation guidelines")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:BracesMustNotBeOmitted", Justification = "Following Frends documentation guidelines")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Documentation checked by custom analyzers")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:FileMustHaveHeader", Justification = "Following Frends documentation guidelines")]
Loading
Loading