diff --git a/src/Elastic.Documentation.Tooling/FileSystems/ChangelogFileSystem.cs b/src/Elastic.Documentation.Tooling/FileSystems/ChangelogFileSystem.cs index 266d91f06..db4abf0c7 100644 --- a/src/Elastic.Documentation.Tooling/FileSystems/ChangelogFileSystem.cs +++ b/src/Elastic.Documentation.Tooling/FileSystems/ChangelogFileSystem.cs @@ -10,13 +10,14 @@ namespace Elastic.Documentation.FileSystems; /// /// Scope for changelog commands: the git root of the target repository. -/// Allows reading .git metadata (remote URL, branch); does not include -/// AppData or build artifacts — changelog operates only within the repo working tree. +/// Allows reading .git metadata (remote URL, branch) and writing under +/// the conventional .artifacts CI staging directory. Does not include +/// AppData — changelog operates only within the repo working tree. /// public class ChangelogFileSystem(IDirectoryInfo root, IFileSystem? inner = null) : ScopedFileSystem(inner ?? Physical, new ScopedFileSystemOptions([root.FullName]) { - AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" }, + AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" }, AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" } }), IChangelogFileSystem diff --git a/tests/Elastic.Changelog.Tests/Evaluation/ChangelogCiFileSystemTests.cs b/tests/Elastic.Changelog.Tests/Evaluation/ChangelogCiFileSystemTests.cs new file mode 100644 index 000000000..6ba926d37 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/Evaluation/ChangelogCiFileSystemTests.cs @@ -0,0 +1,70 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO.Abstractions.TestingHelpers; +using AwesomeAssertions; +using Elastic.Documentation.Configuration; +using Elastic.Documentation.FileSystems; +using Nullean.ScopedFileSystem; + +namespace Elastic.Changelog.Tests.Evaluation; + +/// +/// Regression: changelog CI staging uses .artifacts/changelog-* under the checkout. +/// ScopedFileSystem rejects hidden path segments unless they are allowlisted. +/// +public class ChangelogCiFileSystemTests +{ + private static readonly string Root = Paths.WorkingDirectoryRoot.FullName; + + [Fact] + public void ChangelogFileSystem_AllowsArtifactsStagingAndArtifactDirs() + { + var mock = new MockFileSystem(new MockFileSystemOptions { CurrentDirectory = Root }); + var fs = ChangelogFileSystem.FromWorkingDirectory(mock); + + var staging = Path.Join(Root, ".artifacts", "changelog-staging"); + var artifact = Path.Join(Root, ".artifacts", "changelog-artifact"); + var stagingFile = Path.Join(staging, "42.yaml"); + var artifactFile = Path.Join(artifact, "metadata.json"); + + var create = () => + { + fs.Directory.CreateDirectory(staging); + fs.Directory.CreateDirectory(artifact); + fs.File.WriteAllText(stagingFile, "title: test"); + fs.File.WriteAllText(artifactFile, "{}"); + }; + + create.Should().NotThrow(); + fs.File.Exists(stagingFile).Should().BeTrue(); + fs.File.Exists(artifactFile).Should().BeTrue(); + } + + [Fact] + public void ChangelogFileSystem_BlocksOtherHiddenDirectories() + { + var mock = new MockFileSystem(new MockFileSystemOptions { CurrentDirectory = Root }); + var fs = ChangelogFileSystem.FromWorkingDirectory(mock); + var hidden = Path.Join(Root, ".hidden", "nested"); + + var create = () => fs.Directory.CreateDirectory(hidden); + + create.Should().Throw() + .WithMessage("*hidden*"); + } + + [Fact] + public void RunnerTempFileSystem_BlocksOtherHiddenDirectories() + { + var mock = new MockFileSystem(new MockFileSystemOptions { CurrentDirectory = Root }); + var fs = new RunnerTempFileSystem(mock.DirectoryInfo.New(Root), inner: mock); + var hidden = Path.Join(Root, ".hidden", "nested"); + + var create = () => fs.Directory.CreateDirectory(hidden); + + create.Should().Throw() + .WithMessage("*hidden*"); + } +}