Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ namespace Elastic.Documentation.FileSystems;

/// <summary>
/// Scope for changelog commands: the git root of the target repository.
/// Allows reading <c>.git</c> metadata (remote URL, branch); does not include
/// AppData or build artifacts — changelog operates only within the repo working tree.
/// Allows reading <c>.git</c> metadata (remote URL, branch) and writing under
/// the conventional <c>.artifacts</c> CI staging directory. Does not include
/// AppData — changelog operates only within the repo working tree.
/// </summary>
public class ChangelogFileSystem(IDirectoryInfo root, IFileSystem? inner = null)
: ScopedFileSystem(inner ?? Physical, new ScopedFileSystemOptions([root.FullName])
{
AllowedHiddenFolderNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".git" },
AllowedHiddenFolderNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
AllowedHiddenFileNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".git" }
}),
IChangelogFileSystem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ namespace Elastic.Documentation.FileSystems;
/// <para>
/// Used by <c>evaluate-pr</c>, <c>evaluate-artifact</c>, and <c>prepare-artifact</c> —
/// all three operate on paths vended by the CI environment, not a fixed docset or checkout.
/// Permits <c>.git</c> so changelog configuration can be located by the config loader.
/// Permits <c>.git</c> so changelog configuration can be located by the config loader,
/// and <c>.artifacts</c> so CI staging/artifact paths under the conventional build
/// output directory are writable.
/// </para>
/// </summary>
public class RunnerTempFileSystem(
Expand Down Expand Up @@ -49,7 +51,7 @@ private static ScopedFileSystemOptions BuildOptions(IDirectoryInfo workingRoot,

return new ScopedFileSystemOptions([.. roots])
{
AllowedHiddenFolderNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".git" },
AllowedHiddenFolderNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
AllowedHiddenFileNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".git" }
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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;

/// <summary>
/// Regression: changelog CI staging uses <c>.artifacts/changelog-*</c> under the checkout.
/// ScopedFileSystem rejects hidden path segments unless they are allowlisted.
/// </summary>
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<ScopedFileSystemException>()
.WithMessage("*hidden*");
}

[Fact]
public void RunnerTempFileSystem_AllowsArtifactsStagingAndArtifactDirs()
{
var mock = new MockFileSystem(new MockFileSystemOptions { CurrentDirectory = Root });
var fs = new RunnerTempFileSystem(mock.DirectoryInfo.New(Root), inner: 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 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<ScopedFileSystemException>()
.WithMessage("*hidden*");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,38 @@ public async Task PrepareArtifact_MissingStagingYaml_StatusError()
metadata.Status.Should().Be("error");
}

[Fact]
public async Task PrepareArtifact_ArtifactsPaths_CopiesYamlAndWritesMetadata()
{
// Regression: docs-actions stages under .artifacts/changelog-*. Without
// .artifacts on the ScopedFileSystem allowlist, CreateDirectory throws
// even when generation was skipped.
var stagingDir = Path.Join(Root, ".artifacts", "changelog-staging");
var outputDir = Path.Join(Root, ".artifacts", "changelog-artifact");
await SetupConfig();
FileSystem.Directory.CreateDirectory(stagingDir);
await FileSystem.File.WriteAllTextAsync(
Path.Join(stagingDir, "42.yaml"),
"title: test changelog",
TestContext.Current.CancellationToken);

var service = CreateService();
var args = DefaultArgs() with
{
StagingDir = stagingDir,
OutputDir = outputDir
};

var result = await service.PrepareArtifact(Collector, args, TestContext.Current.CancellationToken);

result.Should().BeTrue();
FileSystem.File.Exists(Path.Join(outputDir, "42.yaml")).Should().BeTrue();
var json = FileSystem.File.ReadAllText(Path.Join(outputDir, "metadata.json"));
var metadata = JsonSerializer.Deserialize(json, ChangelogArtifactMetadataJsonContext.Default.ChangelogArtifactMetadata)!;
metadata.Status.Should().Be("success");
metadata.ChangelogFilename.Should().Be("42.yaml");
}

[Fact]
public async Task PrepareArtifact_WithBomPrefixedYaml_NormalizesOutput()
{
Expand Down
Loading