diff --git a/Directory.Packages.props b/Directory.Packages.props
index 56b21debb7..5a890837dc 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -42,6 +42,7 @@
+
diff --git a/src/Elastic.Codex/Building/CodexBuildService.cs b/src/Elastic.Codex/Building/CodexBuildService.cs
index 71d6a5a43b..d82ce54115 100644
--- a/src/Elastic.Codex/Building/CodexBuildService.cs
+++ b/src/Elastic.Codex/Building/CodexBuildService.cs
@@ -25,6 +25,7 @@
using Elastic.Markdown.Exporters;
using Elastic.Markdown.IO;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Codex.Building;
@@ -46,7 +47,7 @@ public class CodexBuildService(
public async Task BuildAll(
CodexContext context,
CodexCloneResult cloneResult,
- IFileSystem fileSystem,
+ ScopedFileSystem fileSystem,
Cancel ctx,
IReadOnlySet? exporters = null)
{
@@ -66,7 +67,7 @@ public async Task BuildAll(
var buildContexts = new List();
var environment = context.Configuration.Environment ?? "internal";
- using var codexLinkIndexReader = new GitLinkIndexReader(environment, context.ReadFileSystem, skipFetch: true);
+ using var codexLinkIndexReader = new GitLinkIndexReader(environment, FileSystemFactory.AppData, skipFetch: true);
// Phase 1: Load and parse all documentation sets
foreach (var checkout in cloneResult.Checkouts)
@@ -136,7 +137,7 @@ public async Task BuildAll(
private async Task LoadDocumentationSet(
CodexContext context,
CodexCheckout checkout,
- IFileSystem fileSystem,
+ ScopedFileSystem fileSystem,
ILinkIndexReader codexLinkIndexReader,
Cancel ctx)
{
@@ -401,10 +402,10 @@ internal sealed class CodexDocumentationContext(CodexContext codexContext) : ICo
public IDiagnosticsCollector Collector => codexContext.Collector;
///
- public IFileSystem ReadFileSystem => codexContext.ReadFileSystem;
+ public ScopedFileSystem ReadFileSystem => codexContext.ReadFileSystem;
///
- public IFileSystem WriteFileSystem => codexContext.WriteFileSystem;
+ public ScopedFileSystem WriteFileSystem => codexContext.WriteFileSystem;
///
public IDirectoryInfo OutputDirectory => codexContext.OutputDirectory;
diff --git a/src/Elastic.Codex/CodexContext.cs b/src/Elastic.Codex/CodexContext.cs
index f8ff2104e9..0f2f544d5c 100644
--- a/src/Elastic.Codex/CodexContext.cs
+++ b/src/Elastic.Codex/CodexContext.cs
@@ -6,6 +6,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Codex;
using Elastic.Documentation.Diagnostics;
+using Nullean.ScopedFileSystem;
namespace Elastic.Codex;
@@ -14,8 +15,8 @@ namespace Elastic.Codex;
///
public class CodexContext
{
- public IFileSystem ReadFileSystem { get; }
- public IFileSystem WriteFileSystem { get; }
+ public ScopedFileSystem ReadFileSystem { get; }
+ public ScopedFileSystem WriteFileSystem { get; }
public IDiagnosticsCollector Collector { get; }
public CodexConfiguration Configuration { get; }
public IFileInfo ConfigurationPath { get; }
@@ -34,8 +35,8 @@ public CodexContext(
CodexConfiguration configuration,
IFileInfo configurationPath,
IDiagnosticsCollector collector,
- IFileSystem readFileSystem,
- IFileSystem writeFileSystem,
+ ScopedFileSystem readFileSystem,
+ ScopedFileSystem writeFileSystem,
string? checkoutDirectory,
string? outputDirectory)
{
@@ -45,10 +46,12 @@ public CodexContext(
ReadFileSystem = readFileSystem;
WriteFileSystem = writeFileSystem;
- var defaultCheckoutDirectory = Path.Join(Paths.GitCommonRoot.FullName, ".artifacts", "codex", "clone");
- CheckoutDirectory = ReadFileSystem.DirectoryInfo.New(checkoutDirectory ?? defaultCheckoutDirectory);
+ var defaultCheckoutDirectory = Path.Join(Paths.ApplicationData.FullName, "codex", "clone");
+ CheckoutDirectory = checkoutDirectory is null
+ ? FileSystemFactory.AppData.DirectoryInfo.New(defaultCheckoutDirectory)
+ : ReadFileSystem.DirectoryInfo.New(checkoutDirectory);
var defaultOutputDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "codex", "docs");
- OutputDirectory = ReadFileSystem.DirectoryInfo.New(outputDirectory ?? defaultOutputDirectory);
+ OutputDirectory = WriteFileSystem.DirectoryInfo.New(outputDirectory ?? defaultOutputDirectory);
}
}
diff --git a/src/Elastic.Codex/Indexing/CodexIndexService.cs b/src/Elastic.Codex/Indexing/CodexIndexService.cs
index 5a4de17046..c1235c58be 100644
--- a/src/Elastic.Codex/Indexing/CodexIndexService.cs
+++ b/src/Elastic.Codex/Indexing/CodexIndexService.cs
@@ -10,6 +10,7 @@
using Elastic.Documentation.Isolated;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Codex.Indexing;
@@ -30,7 +31,7 @@ IsolatedBuildService isolatedBuildService
public async Task Index(
CodexContext codexContext,
CodexCloneResult cloneResult,
- FileSystem fileSystem,
+ ScopedFileSystem fileSystem,
ElasticsearchIndexOptions esOptions,
Cancel ctx = default)
{
diff --git a/src/Elastic.Documentation.Configuration/BuildContext.cs b/src/Elastic.Documentation.Configuration/BuildContext.cs
index 64301d8333..85db4b731a 100644
--- a/src/Elastic.Documentation.Configuration/BuildContext.cs
+++ b/src/Elastic.Documentation.Configuration/BuildContext.cs
@@ -13,6 +13,7 @@
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Diagnostics;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Configuration;
@@ -21,8 +22,8 @@ public record BuildContext : IDocumentationSetContext, IDocumentationConfigurati
public static string Version { get; } = Assembly.GetExecutingAssembly().GetCustomAttributes()
.FirstOrDefault()?.InformationalVersion ?? "0.0.0";
- public IFileSystem ReadFileSystem { get; }
- public IFileSystem WriteFileSystem { get; }
+ public ScopedFileSystem ReadFileSystem { get; }
+ public ScopedFileSystem WriteFileSystem { get; }
public IReadOnlySet AvailableExporters { get; }
public IDirectoryInfo? DocumentationCheckoutDirectory { get; }
@@ -72,7 +73,7 @@ public string? UrlPathPrefix
public BuildContext(
IDiagnosticsCollector collector,
- IFileSystem fileSystem,
+ ScopedFileSystem fileSystem,
IConfigurationContext configurationContext
)
: this(collector, fileSystem, fileSystem, configurationContext, ExportOptions.Default, null, null)
@@ -81,8 +82,8 @@ IConfigurationContext configurationContext
public BuildContext(
IDiagnosticsCollector collector,
- IFileSystem readFileSystem,
- IFileSystem writeFileSystem,
+ ScopedFileSystem readFileSystem,
+ ScopedFileSystem writeFileSystem,
IConfigurationContext configurationContext,
IReadOnlySet availableExporters,
string? source = null,
@@ -107,7 +108,7 @@ public BuildContext(
(DocumentationSourceDirectory, ConfigurationPath) = Paths.FindDocsFolderFromRoot(ReadFileSystem, rootFolder);
- DocumentationCheckoutDirectory = Paths.DetermineSourceDirectoryRoot(DocumentationSourceDirectory);
+ DocumentationCheckoutDirectory = Paths.FindGitRoot(DocumentationSourceDirectory);
OutputDirectory = !string.IsNullOrWhiteSpace(output)
? WriteFileSystem.DirectoryInfo.New(output)
diff --git a/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs b/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs
index c9eb037037..4fd9970baf 100644
--- a/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs
+++ b/src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs
@@ -45,7 +45,11 @@ public ConfigurationFileProvider(
_fileSystem = fileSystem;
_assemblyName = typeof(ConfigurationFileProvider).Assembly.GetName().Name!;
SkipPrivateRepositories = skipPrivateRepositories;
- TemporaryDirectory = fileSystem.Directory.CreateTempSubdirectory("docs-builder-config");
+ // Use a unique subdirectory per instance to avoid file-locking collisions when
+ // multiple processes or parallel tests share the same ApplicationData path.
+ var configRuntimeDir = Path.Join(Paths.ApplicationData.FullName, "config-runtime", Guid.NewGuid().ToString("N"));
+ TemporaryDirectory = fileSystem.DirectoryInfo.New(configRuntimeDir);
+ TemporaryDirectory.Create();
// TODO: This doesn't work as expected if a github actions consumer repo has a `config` directory.
// ConfigurationSource = configurationSource ?? (
@@ -267,7 +271,7 @@ public static IServiceCollection AddConfigurationFileProvider(this IServiceColle
{
using var sp = services.BuildServiceProvider();
var logFactory = sp.GetRequiredService();
- var provider = new ConfigurationFileProvider(logFactory, new FileSystem(), skipPrivateRepositories, configurationSource);
+ var provider = new ConfigurationFileProvider(logFactory, FileSystemFactory.RealRead, skipPrivateRepositories, configurationSource);
_ = services.AddSingleton(provider);
configure(services, provider);
return services;
diff --git a/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj b/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj
index ff9d136a49..83887a031e 100644
--- a/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj
+++ b/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj
@@ -9,15 +9,17 @@
-
+
+
+
-
-
+
+
diff --git a/src/Elastic.Documentation.Configuration/FileSystemFactory.cs b/src/Elastic.Documentation.Configuration/FileSystemFactory.cs
new file mode 100644
index 0000000000..50870d5d53
--- /dev/null
+++ b/src/Elastic.Documentation.Configuration/FileSystemFactory.cs
@@ -0,0 +1,211 @@
+// 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;
+using System.IO.Abstractions.TestingHelpers;
+using Nullean.ScopedFileSystem;
+
+namespace Elastic.Documentation.Configuration;
+
+public static class FileSystemFactory
+{
+ // Read options: workspace + app data, all confirmed hidden names allowed.
+ // Includes .git (GitCheckoutInformation reads it) and .artifacts/.doc.state
+ // (incremental build reads existing output state).
+ private static readonly ScopedFileSystemOptions WorkingDirectoryReadOptions = new(
+ [Paths.WorkingDirectoryRoot.FullName, Paths.ApplicationData.FullName])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state" }
+ };
+
+ // Write options: same scope roots but no .git — nothing in the build output
+ // pipeline should ever write into the git repository metadata.
+ // Temp is allowed because deploy operations (e.g. S3 sync) stage files there.
+ private static readonly ScopedFileSystemOptions WorkingDirectoryWriteOptions = new(
+ [Paths.WorkingDirectoryRoot.FullName, Paths.ApplicationData.FullName])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".artifacts" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".doc.state" },
+ AllowedSpecialFolders = AllowedSpecialFolder.Temp
+ };
+
+ // AppData-only options: for components that only access caches/state files.
+ private static readonly ScopedFileSystemOptions AppDataOptions = new([Paths.ApplicationData.FullName])
+ {
+ // .git needed for codex-link-index clone directory inside ApplicationData
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git" }
+ };
+
+ ///
+ /// A pre-allocated for reading workspace files.
+ /// Scoped to the working directory root and per-user app data; allows .git
+ /// (read by GitCheckoutInformation), .artifacts and .doc.state
+ /// (read for incremental build state).
+ ///
+ public static ScopedFileSystem RealRead { get; } = new(new FileSystem(), WorkingDirectoryReadOptions);
+
+ ///
+ /// A pre-allocated for writing build output.
+ /// Same scope as but without .git access —
+ /// nothing in the output pipeline should write into git repository metadata.
+ ///
+ public static ScopedFileSystem RealWrite { get; } = new(new FileSystem(), WorkingDirectoryWriteOptions);
+
+ ///
+ /// A pre-allocated scoped only to the per-user
+ /// elastic/docs-builder application data folder. Use for components that
+ /// access caches or state and have no need for workspace files
+ /// (e.g. CrossLinkFetcher, CheckForUpdatesFilter, GitLinkIndexReader).
+ ///
+ public static ScopedFileSystem AppData { get; } = new(new FileSystem(), AppDataOptions);
+
+ ///
+ /// Creates a new wrapping a fresh ,
+ /// using the working-directory read options. Each call returns a new independent in-memory file system.
+ ///
+ public static ScopedFileSystem InMemory() => new(new MockFileSystem(), WorkingDirectoryReadOptions);
+
+ ///
+ /// Scopes to and
+ /// for reading. Use when the inner FS contains files
+ /// that live within the current working-directory tree (e.g. a test MockFileSystem
+ /// seeded with workspace-relative paths).
+ ///
+ public static ScopedFileSystem ScopeCurrentWorkingDirectory(IFileSystem inner) =>
+ new(inner, WorkingDirectoryReadOptions);
+
+ ///
+ /// Scopes to and
+ /// for reading, extended by
+ /// (e.g. detection-rules folders declared via
+ /// ).
+ ///
+ public static ScopedFileSystem ScopeCurrentWorkingDirectory(IFileSystem inner, IEnumerable? extensionRoots)
+ {
+ if (extensionRoots is null)
+ return ScopeCurrentWorkingDirectory(inner);
+
+ var roots = new[] { Paths.WorkingDirectoryRoot.FullName, Paths.ApplicationData.FullName }
+ .Concat(extensionRoots)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+ if (roots.Length == 2)
+ return ScopeCurrentWorkingDirectory(inner);
+
+ return new ScopedFileSystem(inner, new ScopedFileSystemOptions(roots)
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state" }
+ });
+ }
+
+ // Builds write options that include AllowedSpecialFolders.Temp PLUS the inner FS's own
+ // GetTempPath() as an explicit root — but only when the inner FS is MockFileSystem.
+ //
+ // On non-Windows MockFileSystem hardcodes a Unix-ified path ("/temp/", derived from "C:\temp")
+ // instead of calling System.IO.Path.GetTempPath(). AllowedSpecialFolder.Temp uses the real
+ // GetTempPath() (e.g. "/tmp/" on Linux), so the two diverge and scope validation fails for any
+ // path created via mockFs.Path.GetTempPath().
+ //
+ // Fix tracked upstream: https://github.com/TestableIO/System.IO.Abstractions/pull/1454
+ // Once that ships and we update the package reference we can drop this workaround.
+ //
+ // We use ScopedFileSystem.InnerType (added in Nullean.ScopedFileSystem 0.4.0) to avoid a
+ // fragile string-based type check.
+ private static ScopedFileSystemOptions BuildWriteOptions(IFileSystem inner, params string[] roots)
+ {
+ var allRoots = roots.ToList();
+ var innerType = inner is ScopedFileSystem sf ? sf.InnerType : inner.GetType();
+ if (!OperatingSystem.IsWindows() && innerType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase))
+ {
+ // Cover MockFileSystem's unixified hardcoded temp path
+ var innerTemp = inner.Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ if (!string.IsNullOrEmpty(innerTemp) && !allRoots.Contains(innerTemp, StringComparer.OrdinalIgnoreCase))
+ allRoots.Add(innerTemp);
+ }
+ return new ScopedFileSystemOptions([.. allRoots])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".artifacts" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".doc.state" },
+ AllowedSpecialFolders = AllowedSpecialFolder.Temp
+ };
+ }
+
+ ///
+ /// Scopes to and
+ /// for writing (.git not allowed). Use when
+ /// the inner FS writes into the working-directory tree.
+ ///
+ public static ScopedFileSystem ScopeCurrentWorkingDirectoryForWrite(IFileSystem inner) =>
+ new(inner, BuildWriteOptions(
+ inner, Paths.WorkingDirectoryRoot.FullName, Paths.ApplicationData.FullName));
+
+ ///
+ /// Scopes to an explicit and
+ /// for reading. Use when the files to be read live under
+ /// a specific known root that is not — for example
+ /// test fixtures with assembler-checkout paths or service code operating on a given directory.
+ ///
+ public static ScopedFileSystem ScopeSourceDirectory(IFileSystem inner, string sourceRoot) =>
+ new(inner, new ScopedFileSystemOptions([sourceRoot, Paths.ApplicationData.FullName])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state" }
+ });
+
+ ///
+ /// Scopes to an explicit and
+ /// for writing (.git not allowed). Write variant
+ /// of .
+ ///
+ public static ScopedFileSystem ScopeSourceDirectoryForWrite(IFileSystem inner, string sourceRoot) =>
+ new(inner, BuildWriteOptions(inner, sourceRoot, Paths.ApplicationData.FullName));
+
+ ///
+ /// Creates a read scoped to the git root of
+ /// . Falls back to when
+ /// is . Use in commands that accept an explicit --path argument.
+ ///
+ /// Suitable for command-layer code. Service-layer tests use directly
+ /// and do not exercise this method.
+ ///
+ ///
+ public static ScopedFileSystem RealGitRootForPath(string? path)
+ {
+ if (path is null)
+ return RealRead;
+ var root = Paths.FindGitRoot(path);
+ return new ScopedFileSystem(new FileSystem(), new ScopedFileSystemOptions([root, Paths.ApplicationData.FullName])
+ {
+ AllowedHiddenFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".artifacts" },
+ AllowedHiddenFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) { ".git", ".doc.state" }
+ });
+ }
+
+ ///
+ /// Creates a write scoped to the git root of
+ /// (and if it falls outside that root).
+ /// Falls back to when both are .
+ /// Use in commands that accept explicit --path and/or --output arguments.
+ ///
+ public static ScopedFileSystem RealGitRootForPathWrite(string? path, string? output = null)
+ {
+ if (path is null && output is null)
+ return RealWrite;
+
+ var gitRoot = path is not null ? Paths.FindGitRoot(path) : Paths.WorkingDirectoryRoot.FullName;
+ var roots = new List { gitRoot, Paths.ApplicationData.FullName };
+
+ var plain = new FileSystem();
+ if (output is not null)
+ {
+ var absOutput = Path.IsPathRooted(output) ? output : Path.GetFullPath(output);
+ if (!plain.DirectoryInfo.New(absOutput).IsSubPathOf(plain.DirectoryInfo.New(gitRoot)))
+ roots.Add(absOutput);
+ }
+
+ return new ScopedFileSystem(plain, BuildWriteOptions(plain, [.. roots]));
+ }
+}
diff --git a/src/Elastic.Documentation.Configuration/Paths.cs b/src/Elastic.Documentation.Configuration/Paths.cs
index 47d2468074..bd3e2085be 100644
--- a/src/Elastic.Documentation.Configuration/Paths.cs
+++ b/src/Elastic.Documentation.Configuration/Paths.cs
@@ -11,79 +11,113 @@ public static class Paths
{
public static readonly DirectoryInfo WorkingDirectoryRoot = DetermineWorkingDirectoryRoot();
- public static readonly DirectoryInfo GitCommonRoot = InitGitCommonRoot();
-
public static readonly DirectoryInfo ApplicationData = GetApplicationFolder();
- private static DirectoryInfo DetermineWorkingDirectoryRoot()
+ ///
+ /// Walks up from until a .git directory or file
+ /// (worktree pointer) is found and returns that ancestor. Returns
+ /// itself when no git root is found within the allowed depth.
+ ///
+ ///
+ /// Depth protection: in release builds the .git anchor must be at most 1 directory
+ /// above — documentation is not expected to live deep inside
+ /// a repo. In debug builds a deeper .git is accepted when a *.slnx file is
+ /// adjacent (developer running the binary from an IDE output directory).
+ ///
+ public static string FindGitRoot(string startPath)
{
- var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
- while (directory != null)
+ var resolved = Path.IsPathRooted(startPath) ? startPath : Path.GetFullPath(startPath);
+ var dir = Directory.Exists(resolved)
+ ? new DirectoryInfo(resolved)
+ : new DirectoryInfo(Path.GetDirectoryName(resolved) ?? resolved);
+ var startDir = dir.FullName; // always a directory, used as fallback
+ var depth = 0;
+ while (dir != null)
{
- if (directory.GetFiles("*.slnx").Length > 0)
- break;
- if (directory.GetDirectories(".git").Length > 0)
- break;
- // support for git worktrees
- if (directory.GetFiles(".git").Length > 0)
- break;
- directory = directory.Parent;
+ var hasGit = dir.GetDirectories(".git").Length > 0 || dir.GetFiles(".git").Length > 0;
+ if (hasGit)
+ {
+#if DEBUG
+ if (depth <= 1 || dir.GetFiles("*.slnx").Length > 0)
+ return dir.FullName;
+#else
+ if (depth <= 1)
+ return dir.FullName;
+#endif
+ // .git found but too deep — stop searching
+ return startDir;
+ }
+ depth++;
+ dir = dir.Parent;
}
- return directory ?? new DirectoryInfo(Directory.GetCurrentDirectory());
+ return startDir;
}
- public static IDirectoryInfo? DetermineSourceDirectoryRoot(IDirectoryInfo sourceDirectory)
+ ///
+ /// Walks up from via until
+ /// a .git directory or file (worktree pointer) is found.
+ /// Returns if no git root is found within the allowed depth.
+ ///
+ /// Same depth protection as .
+ public static IDirectoryInfo? FindGitRoot(IDirectoryInfo startDirectory)
{
- IDirectoryInfo? sourceRoot = null;
- var directory = sourceDirectory;
- while (directory != null && directory.GetDirectories(".git").Length == 0)
+ var directory = startDirectory;
+ var depth = 0;
+ while (directory != null)
{
- if (directory.GetDirectories(".git").Length > 0)
- break;
- // support for git worktrees
- if (directory.GetFiles(".git").Length > 0)
- break;
-
+ var hasGit = directory.GetDirectories(".git").Length > 0
+ || directory.GetFiles(".git").Length > 0;
+ if (hasGit)
+ {
+#if DEBUG
+ if (depth <= 1 || directory.GetFiles("*.slnx").Length > 0)
+ return directory;
+#else
+ if (depth <= 1)
+ return directory;
+#endif
+ // .git found but too deep
+ return null;
+ }
+ depth++;
directory = directory.Parent;
}
- sourceRoot ??= directory;
- return sourceRoot;
- }
-
- /// Resolves the root of the main git repository, following worktree links when present. Disabled on CI.
- public static IDirectoryInfo ResolveGitCommonRoot(IFileSystem fileSystem, IDirectoryInfo workingDirectoryRoot, bool? isCI = null)
- {
- if (isCI ?? !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")))
- return workingDirectoryRoot;
-
- var gitPath = Path.Join(workingDirectoryRoot.FullName, ".git");
-
- if (fileSystem.Directory.Exists(gitPath))
- return workingDirectoryRoot;
-
- if (!fileSystem.File.Exists(gitPath))
- return workingDirectoryRoot;
-
- var content = fileSystem.File.ReadAllText(gitPath).Trim();
- if (!content.StartsWith("gitdir:", StringComparison.OrdinalIgnoreCase))
- return workingDirectoryRoot;
-
- var gitDirPath = content["gitdir:".Length..].Trim();
- if (!Path.IsPathRooted(gitDirPath))
- gitDirPath = Path.GetFullPath(gitDirPath, workingDirectoryRoot.FullName);
-
- var dir = fileSystem.DirectoryInfo.New(gitDirPath);
- while (dir != null && dir.Name != ".git")
- dir = dir.Parent;
-
- return dir?.Parent ?? workingDirectoryRoot;
+ return null;
}
- private static DirectoryInfo InitGitCommonRoot()
+ private static DirectoryInfo DetermineWorkingDirectoryRoot()
{
- var fs = new FileSystem();
- var root = fs.DirectoryInfo.New(WorkingDirectoryRoot.FullName);
- return new DirectoryInfo(ResolveGitCommonRoot(fs, root).FullName);
+ var cwd = new DirectoryInfo(Directory.GetCurrentDirectory());
+ var directory = cwd;
+ var depth = 0;
+ while (directory != null)
+ {
+ // *.slnx is the primary anchor: always adopt it at any depth.
+ // This covers both the local developer case (running from the IDE output directory
+ // such as bin/Debug/net10.0/) and CI (Aspire starts the binary from the project
+ // directory, which is several levels below the solution root).
+ if (directory.GetFiles("*.slnx").Length > 0)
+ return directory;
+ var hasGit = directory.GetDirectories(".git").Length > 0
+ || directory.GetFiles(".git").Length > 0;
+ if (hasGit)
+ {
+ // Only accept .git beyond 1 level up in debug when a *.slnx is adjacent
+ // (developer running from IDE output directory such as bin/Debug/net10.0/).
+#if DEBUG
+ if (depth <= 1 || directory.GetFiles("*.slnx").Length > 0)
+ return directory;
+#else
+ if (depth <= 1)
+ return directory;
+#endif
+ // .git found but too deep — stop without adopting it
+ return cwd;
+ }
+ depth++;
+ directory = directory.Parent;
+ }
+ return cwd;
}
/// Used in debug to locate static folder, so we can change js/css files while the server is running
@@ -101,6 +135,14 @@ private static DirectoryInfo InitGitCommonRoot()
private static DirectoryInfo GetApplicationFolder()
{
var localPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ if (string.IsNullOrEmpty(localPath))
+ {
+ // Docker / CI containers often have no XDG_DATA_HOME or HOME configured,
+ // causing LocalApplicationData to return "". Path.Join("", ...) produces a
+ // relative path that resolves under CWD, becoming a subdirectory of
+ // WorkingDirectoryRoot and breaking the disjoint-scope-roots requirement.
+ localPath = Path.GetTempPath();
+ }
var elasticPath = Path.Join(localPath, "elastic", "docs-builder");
return new DirectoryInfo(elasticPath);
}
diff --git a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs
index 9ba12a78bf..c2bf385b95 100644
--- a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs
+++ b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs
@@ -7,6 +7,7 @@
using Elastic.Documentation.Configuration.Toc.DetectionRules;
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Extensions;
+using Nullean.ScopedFileSystem;
using YamlDotNet.Serialization;
using static Elastic.Documentation.Configuration.SymlinkValidator;
@@ -96,9 +97,9 @@ public static DocumentationSetFile LoadMetadata(IFileInfo file)
/// replacing them with their resolved children and ensuring file paths carry over parent paths.
/// Validates the table of contents structure and emits diagnostics for issues.
///
- public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collector, IFileInfo docsetPath, IFileSystem? fileSystem = null, HashSet? noSuppress = null)
+ public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collector, IFileInfo docsetPath, ScopedFileSystem? fileSystem = null, HashSet? noSuppress = null)
{
- fileSystem ??= docsetPath.FileSystem;
+ fileSystem ??= FileSystemFactory.ScopeSourceDirectory(docsetPath.FileSystem, docsetPath.Directory!.FullName);
// Validate that the docset.yml is not a symlink (security: prevents path traversal attacks)
EnsureNotSymlink(docsetPath);
var yaml = fileSystem.File.ReadAllText(docsetPath.FullName);
@@ -118,9 +119,9 @@ public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collecto
/// replacing them with their resolved children and ensuring file paths carry over parent paths.
/// Validates the table of contents structure and emits diagnostics for issues.
///
- public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collector, string yaml, IDirectoryInfo sourceDirectory, IFileSystem? fileSystem = null, HashSet? noSuppress = null)
+ public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collector, string yaml, IDirectoryInfo sourceDirectory, ScopedFileSystem? fileSystem = null, HashSet? noSuppress = null)
{
- fileSystem ??= sourceDirectory.FileSystem;
+ fileSystem ??= FileSystemFactory.ScopeSourceDirectory(sourceDirectory.FileSystem, sourceDirectory.FullName);
var docSet = Deserialize(yaml);
var docsetPath = fileSystem.Path.Join(sourceDirectory.FullName, "docset.yml").OptionalWindowsReplace();
docSet.SuppressDiagnostics.ExceptWith(noSuppress ?? []);
diff --git a/src/Elastic.Documentation.LinkIndex/Elastic.Documentation.LinkIndex.csproj b/src/Elastic.Documentation.LinkIndex/Elastic.Documentation.LinkIndex.csproj
index e86f0f7745..1fef2ce2a0 100644
--- a/src/Elastic.Documentation.LinkIndex/Elastic.Documentation.LinkIndex.csproj
+++ b/src/Elastic.Documentation.LinkIndex/Elastic.Documentation.LinkIndex.csproj
@@ -8,10 +8,12 @@
+
+
diff --git a/src/Elastic.Documentation.LinkIndex/GitLinkIndexReader.cs b/src/Elastic.Documentation.LinkIndex/GitLinkIndexReader.cs
index 901aff790c..d302caa764 100644
--- a/src/Elastic.Documentation.LinkIndex/GitLinkIndexReader.cs
+++ b/src/Elastic.Documentation.LinkIndex/GitLinkIndexReader.cs
@@ -4,7 +4,9 @@
using System.Diagnostics;
using System.IO.Abstractions;
+using Elastic.Documentation.Configuration;
using Elastic.Documentation.Links;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.LinkIndex;
@@ -16,8 +18,7 @@ public class GitLinkIndexReader : ILinkIndexReader, IDisposable
{
private const string LinkIndexOrigin = "elastic/codex-link-index";
private static readonly string CloneDirectory = Path.Join(
- Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
- ".docs-builder",
+ Paths.ApplicationData.FullName,
"codex-link-index");
private readonly string _environment;
@@ -26,13 +27,13 @@ public class GitLinkIndexReader : ILinkIndexReader, IDisposable
private readonly SemaphoreSlim _cloneLock = new(1, 1);
private bool _ensuredClone;
- public GitLinkIndexReader(string environment, IFileSystem? fileSystem = null, bool skipFetch = false)
+ public GitLinkIndexReader(string environment, ScopedFileSystem? fileSystem = null, bool skipFetch = false)
{
if (string.IsNullOrWhiteSpace(environment))
throw new ArgumentException("Environment must be specified in the codex configuration (e.g., 'internal', 'security').", nameof(environment));
_environment = environment;
- _fileSystem = fileSystem ?? new FileSystem();
+ _fileSystem = fileSystem ?? FileSystemFactory.AppData;
_skipFetch = skipFetch;
}
diff --git a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs
index f48379cde9..c0c7d21e76 100644
--- a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs
+++ b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs
@@ -4,11 +4,13 @@
using System.Collections.Concurrent;
using System.Collections.Frozen;
+using System.IO.Abstractions;
using System.Text.Json;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.LinkIndex;
using Elastic.Documentation.Serialization;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Links.CrossLinks;
@@ -42,9 +44,10 @@ public record FetchedCrossLinks
};
}
-public abstract class CrossLinkFetcher(ILoggerFactory logFactory, ILinkIndexReader linkIndexProvider) : IDisposable
+public abstract class CrossLinkFetcher(ILoggerFactory logFactory, ILinkIndexReader linkIndexProvider, ScopedFileSystem? fileSystem = null) : IDisposable
{
protected ILogger Logger { get; } = logFactory.CreateLogger(nameof(CrossLinkFetcher));
+ private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.AppData;
private LinkRegistry? _linkIndex;
public static RepositoryLinks Deserialize(string json) =>
@@ -146,12 +149,12 @@ private void WriteLinksJsonCachedFile(string repository, LinkRegistryEntry linkR
{
var cachedFileName = $"links-elastic-{repository}-{linkRegistryEntry.Branch}-{linkRegistryEntry.ETag}.json";
var cachedPath = Path.Join(Paths.ApplicationData.FullName, "links", cachedFileName);
- if (File.Exists(cachedPath))
+ if (_fileSystem.File.Exists(cachedPath))
return;
try
{
- _ = Directory.CreateDirectory(Path.GetDirectoryName(cachedPath)!);
- File.WriteAllText(cachedPath, RepositoryLinks.Serialize(linkReference));
+ _ = _fileSystem.Directory.CreateDirectory(Path.GetDirectoryName(cachedPath)!);
+ _fileSystem.File.WriteAllText(cachedPath, RepositoryLinks.Serialize(linkReference));
}
catch (Exception e)
{
@@ -168,11 +171,11 @@ private void WriteLinksJsonCachedFile(string repository, LinkRegistryEntry linkR
if (_cachedLinkReferences.TryGetValue(cachedFileName, out var cachedLinkReference))
return cachedLinkReference;
- if (File.Exists(cachedPath))
+ if (_fileSystem.File.Exists(cachedPath))
{
try
{
- var json = await File.ReadAllTextAsync(cachedPath);
+ var json = await _fileSystem.File.ReadAllTextAsync(cachedPath);
var linkReference = Deserialize(json);
_ = _cachedLinkReferences.TryAdd(cachedFileName, linkReference);
return linkReference;
diff --git a/src/Elastic.Documentation/Elastic.Documentation.csproj b/src/Elastic.Documentation/Elastic.Documentation.csproj
index fbc2f8c723..a6a49d02a2 100644
--- a/src/Elastic.Documentation/Elastic.Documentation.csproj
+++ b/src/Elastic.Documentation/Elastic.Documentation.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/Elastic.Documentation/GitCheckoutInformation.cs b/src/Elastic.Documentation/GitCheckoutInformation.cs
index 6981b40ef8..e08b965101 100644
--- a/src/Elastic.Documentation/GitCheckoutInformation.cs
+++ b/src/Elastic.Documentation/GitCheckoutInformation.cs
@@ -7,6 +7,7 @@
using System.Text.RegularExpressions;
using Elastic.Documentation.Extensions;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
using SoftCircuits.IniFileParser;
namespace Elastic.Documentation;
@@ -45,7 +46,11 @@ public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem
if (source is null)
return Unavailable;
- if (fileSystem is not FileSystem)
+ // Return test data for in-memory (mock) file systems. Use ScopedFileSystem.InnerType
+ // (available since Nullean.ScopedFileSystem 0.4.0) to inspect through the scope wrapper
+ // rather than relying on the outer type name.
+ var fsType = fileSystem is ScopedFileSystem sf ? sf.InnerType : fileSystem.GetType();
+ if (fsType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase))
{
return new GitCheckoutInformation
{
diff --git a/src/Elastic.Documentation/IDocumentationContext.cs b/src/Elastic.Documentation/IDocumentationContext.cs
index cf7daaddd1..72512d4c09 100644
--- a/src/Elastic.Documentation/IDocumentationContext.cs
+++ b/src/Elastic.Documentation/IDocumentationContext.cs
@@ -4,14 +4,15 @@
using System.IO.Abstractions;
using Elastic.Documentation.Diagnostics;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation;
public interface IDocumentationContext
{
IDiagnosticsCollector Collector { get; }
- IFileSystem ReadFileSystem { get; }
- IFileSystem WriteFileSystem { get; }
+ ScopedFileSystem ReadFileSystem { get; }
+ ScopedFileSystem WriteFileSystem { get; }
IDirectoryInfo OutputDirectory { get; }
IFileInfo ConfigurationPath { get; }
BuildType BuildType { get; }
diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs
index dc99b1c1d7..82924f929b 100644
--- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs
+++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs
@@ -19,6 +19,14 @@ public class DetectionRulesDocsBuilderExtension(BuildContext build) : IDocsBuild
private BuildContext Build { get; } = build;
private bool _versionLockInitialized;
+ public IEnumerable ExternalScopeRoots =>
+ Build.ConfigurationYaml.TableOfContents
+ .OfType()
+ .SelectMany(f => f.Children.OfType())
+ .SelectMany(r => r.DetectionRuleFolders)
+ .Select(f => Path.GetFullPath(f, Build.DocumentationSourceDirectory.FullName))
+ .Distinct();
+
public IDocumentationFileExporter? FileExporter { get; } = new RuleDocumentationFileExporter(build.ReadFileSystem, build.WriteFileSystem);
public DocumentationFile? CreateDocumentationFile(IFileInfo file, MarkdownParser markdownParser)
diff --git a/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs b/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs
index 1dcba78d65..a1d5af396d 100644
--- a/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs
+++ b/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs
@@ -15,6 +15,12 @@ public interface IDocsBuilderExtension
{
IDocumentationFileExporter? FileExporter { get; }
+ ///
+ /// Directories outside the working directory root that this extension requires read access to.
+ /// These are collected and added as additional ScopedFileSystem roots when building.
+ ///
+ IEnumerable ExternalScopeRoots => [];
+
/// Create an instance of if it matches the .
/// Return `null` to let another extension handle this.
DocumentationFile? CreateDocumentationFile(IFileInfo file, MarkdownParser markdownParser);
diff --git a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvReader.cs b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvReader.cs
index 50b9f85e7e..12f6b1d68f 100644
--- a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvReader.cs
+++ b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvReader.cs
@@ -3,15 +3,17 @@
// See the LICENSE file in the project root for more information
using System.IO.Abstractions;
+using Elastic.Documentation.Configuration;
using nietras.SeparatedValues;
+using Nullean.ScopedFileSystem;
namespace Elastic.Markdown.Myst.Directives.CsvInclude;
public static class CsvReader
{
- public static IEnumerable ReadCsvFile(string filePath, string separator, IFileSystem? fileSystem = null)
+ public static IEnumerable ReadCsvFile(string filePath, string separator, ScopedFileSystem? fileSystem = null)
{
- var fs = fileSystem ?? new FileSystem();
+ var fs = fileSystem ?? FileSystemFactory.RealRead;
return ReadWithSep(filePath, separator, fs);
}
@@ -20,35 +22,16 @@ private static IEnumerable ReadWithSep(string filePath, string separat
var separatorChar = separator == "," ? ',' : separator[0];
var spec = Sep.New(separatorChar);
- // Sep works with actual file paths, not virtual file systems
- // For testing with MockFileSystem, we'll read content first
- if (fileSystem.GetType().Name == "MockFileSystem")
- {
- var content = fileSystem.File.ReadAllText(filePath);
- using var reader = spec.Reader(o => o with { HasHeader = false, Unescape = true }).FromText(content);
+ // Always read via IFileSystem so that scoped and mock file systems are respected.
+ var content = fileSystem.File.ReadAllText(filePath);
+ using var reader = spec.Reader(o => o with { HasHeader = false, Unescape = true }).FromText(content);
- foreach (var row in reader)
- {
- var rowData = new string[row.ColCount];
- for (var i = 0; i < row.ColCount; i++)
- rowData[i] = row[i].ToString();
- yield return rowData;
- }
- }
- else
+ foreach (var row in reader)
{
- using var reader = spec.Reader(o => o with { HasHeader = false, Unescape = true }).FromFile(filePath);
-
- foreach (var row in reader)
- {
- var rowData = new string[row.ColCount];
- for (var i = 0; i < row.ColCount; i++)
- {
- rowData[i] = row[i].ToString();
- }
- yield return rowData;
- }
+ var rowData = new string[row.ColCount];
+ for (var i = 0; i < row.ColCount; i++)
+ rowData[i] = row[i].ToString();
+ yield return rowData;
}
}
-
}
diff --git a/src/authoring/Elastic.Documentation.Refactor/FormatService.cs b/src/authoring/Elastic.Documentation.Refactor/FormatService.cs
index 785089c7bc..591a8c8da9 100644
--- a/src/authoring/Elastic.Documentation.Refactor/FormatService.cs
+++ b/src/authoring/Elastic.Documentation.Refactor/FormatService.cs
@@ -10,6 +10,7 @@
using Elastic.Documentation.Services;
using Elastic.Markdown.IO;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Refactor;
@@ -34,7 +35,7 @@ public async Task Format(
IDiagnosticsCollector collector,
string? path,
bool checkOnly,
- IFileSystem fs,
+ ScopedFileSystem fs,
Cancel ctx
)
{
diff --git a/src/authoring/Elastic.Documentation.Refactor/MoveFileService.cs b/src/authoring/Elastic.Documentation.Refactor/MoveFileService.cs
index 20de1525d7..91fbaedd56 100644
--- a/src/authoring/Elastic.Documentation.Refactor/MoveFileService.cs
+++ b/src/authoring/Elastic.Documentation.Refactor/MoveFileService.cs
@@ -9,6 +9,7 @@
using Elastic.Documentation.Services;
using Elastic.Markdown.IO;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Refactor;
@@ -23,7 +24,7 @@ public async Task Move(
string target,
bool? dryRun,
string? path,
- IFileSystem fs,
+ ScopedFileSystem fs,
Cancel ctx
)
{
diff --git a/src/authoring/Elastic.Documentation.Refactor/Tracking/LocalChangesService.cs b/src/authoring/Elastic.Documentation.Refactor/Tracking/LocalChangesService.cs
index da1e910935..530e46a021 100644
--- a/src/authoring/Elastic.Documentation.Refactor/Tracking/LocalChangesService.cs
+++ b/src/authoring/Elastic.Documentation.Refactor/Tracking/LocalChangesService.cs
@@ -9,6 +9,7 @@
using Elastic.Documentation.Extensions;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Refactor.Tracking;
@@ -19,7 +20,7 @@ IConfigurationContext configurationContext
{
private readonly ILogger _logger = logFactory.CreateLogger();
- public Task ValidateRedirects(IDiagnosticsCollector collector, string? path, FileSystem fs)
+ public Task ValidateRedirects(IDiagnosticsCollector collector, string? path, ScopedFileSystem fs)
{
var runningOnCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"));
@@ -38,7 +39,7 @@ public Task ValidateRedirects(IDiagnosticsCollector collector, string? pat
return Task.FromResult(false);
}
- var root = Paths.DetermineSourceDirectoryRoot(buildContext.DocumentationSourceDirectory);
+ var root = Paths.FindGitRoot(buildContext.DocumentationSourceDirectory);
if (root is null)
{
collector.EmitError(redirectFile.Source, $"Unable to determine the root of the source directory {buildContext.DocumentationSourceDirectory}.");
diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs
index e23300dee3..4e8e3f44e8 100644
--- a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs
+++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs
@@ -16,6 +16,7 @@
using Elastic.Documentation.ReleaseNotes;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Bundling;
@@ -46,13 +47,13 @@ public record AmendBundleArguments
///
public partial class ChangelogBundleAmendService(
ILoggerFactory logFactory,
- IFileSystem? fileSystem = null,
+ ScopedFileSystem? fileSystem = null,
IConfigurationContext? configurationContext = null) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystem();
+ private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
private readonly ChangelogConfigurationLoader? _configLoader = configurationContext != null
- ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem ?? new FileSystem())
+ ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead)
: null;
[GeneratedRegex(@"\.amend-(\d+)\.ya?ml$", RegexOptions.IgnoreCase)]
diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs
index c425b3771f..f113e08e7a 100644
--- a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs
+++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs
@@ -18,6 +18,7 @@
using Elastic.Documentation.ReleaseNotes;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Bundling;
@@ -105,15 +106,15 @@ public record BundleChangelogsArguments
public partial class ChangelogBundlingService(
ILoggerFactory logFactory,
IConfigurationContext? configurationContext = null,
- IFileSystem? fileSystem = null,
+ ScopedFileSystem? fileSystem = null,
IGitHubReleaseService? releaseService = null)
: IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystem();
+ private readonly ScopedFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
private readonly IGitHubReleaseService _releaseService = releaseService ?? new GitHubReleaseService(logFactory);
private readonly ChangelogConfigurationLoader? _configLoader = configurationContext != null
- ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem ?? new FileSystem())
+ ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead)
: null;
[GeneratedRegex(@"(\s+)version:", RegexOptions.Multiline)]
diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs
index 1589c4e418..3f66938565 100644
--- a/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs
+++ b/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs
@@ -12,6 +12,7 @@
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Bundling;
@@ -62,15 +63,15 @@ public record BundleDependency(string ChangelogFile, string BundleFile);
public class ChangelogRemoveService(
ILoggerFactory logFactory,
IConfigurationContext? configurationContext = null,
- IFileSystem? fileSystem = null,
+ ScopedFileSystem? fileSystem = null,
IGitHubReleaseService? releaseService = null)
: IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystem();
+ private readonly ScopedFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
private readonly IGitHubReleaseService _releaseService = releaseService ?? new GitHubReleaseService(logFactory);
private readonly ChangelogConfigurationLoader? _configLoader = configurationContext != null
- ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem ?? new FileSystem())
+ ? new ChangelogConfigurationLoader(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead)
: null;
public async Task RemoveChangelogs(IDiagnosticsCollector collector, ChangelogRemoveArguments input, Cancel ctx)
diff --git a/src/services/Elastic.Changelog/Bundling/ProfileFilterResolver.cs b/src/services/Elastic.Changelog/Bundling/ProfileFilterResolver.cs
index a08dced8f8..a8b39073ed 100644
--- a/src/services/Elastic.Changelog/Bundling/ProfileFilterResolver.cs
+++ b/src/services/Elastic.Changelog/Bundling/ProfileFilterResolver.cs
@@ -11,6 +11,7 @@
using Elastic.Documentation.ReleaseNotes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Bundling;
@@ -79,7 +80,7 @@ public static partial class ProfileFilterResolver
string profileName,
string? profileArgument,
ChangelogConfiguration? config,
- IFileSystem fileSystem,
+ ScopedFileSystem fileSystem,
ILogger? logger,
Cancel ctx,
string? profileReport = null,
@@ -192,7 +193,7 @@ public static partial class ProfileFilterResolver
string profileArgument,
string profileReport,
BundleProfile profile,
- IFileSystem fileSystem,
+ ScopedFileSystem fileSystem,
ILogger? logger,
Cancel ctx)
{
@@ -270,7 +271,7 @@ public static partial class ProfileFilterResolver
internal static async Task ResolveUrlListFileAsync(
IDiagnosticsCollector collector,
string filePath,
- IFileSystem fileSystem,
+ ScopedFileSystem fileSystem,
Cancel ctx)
{
var content = await fileSystem.File.ReadAllTextAsync(filePath, ctx);
@@ -326,7 +327,7 @@ public static partial class ProfileFilterResolver
return hasPrs ? new UrlListFileResult(lines, null) : new UrlListFileResult(null, lines);
}
- private static ProfileArgumentType DetectLocalFileType(IFileSystem fileSystem, string path) =>
+ private static ProfileArgumentType DetectLocalFileType(ScopedFileSystem fileSystem, string path) =>
fileSystem.Path.GetExtension(path).ToLowerInvariant() is ".html" or ".htm"
? ProfileArgumentType.PromotionReportFile
: ProfileArgumentType.UrlListFile;
diff --git a/src/services/Elastic.Changelog/Bundling/PromotionReportParser.cs b/src/services/Elastic.Changelog/Bundling/PromotionReportParser.cs
index b1ed95d351..aeda3acf34 100644
--- a/src/services/Elastic.Changelog/Bundling/PromotionReportParser.cs
+++ b/src/services/Elastic.Changelog/Bundling/PromotionReportParser.cs
@@ -5,18 +5,20 @@
using System.IO.Abstractions;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
+using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Bundling;
///
/// Parser for promotion report HTML files to extract PR lists
///
-public partial class PromotionReportParser(ILoggerFactory logFactory, IFileSystem? fileSystem = null)
+public partial class PromotionReportParser(ILoggerFactory logFactory, ScopedFileSystem? fileSystem = null)
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystem();
+ private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
private static readonly HttpClient HttpClient = new();
static PromotionReportParser()
diff --git a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs
index 2ef0ce90dc..a1a091db81 100644
--- a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs
+++ b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs
@@ -11,6 +11,7 @@
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Creation;
@@ -61,16 +62,16 @@ public class ChangelogCreationService(
ILoggerFactory logFactory,
IConfigurationContext configurationContext,
IGitHubPrService? githubPrService = null,
-IFileSystem? fileSystem = null,
+ScopedFileSystem? fileSystem = null,
IEnvironmentVariables? env = null
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem ?? new FileSystem());
+ private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead);
private readonly CreateChangelogArgumentsValidator _validator = new(configurationContext);
private readonly PrInfoProcessor _prProcessor = new(githubPrService, logFactory.CreateLogger());
private readonly IssueInfoProcessor _issueProcessor = new(githubPrService, logFactory.CreateLogger());
- private readonly ChangelogFileWriter _fileWriter = new(fileSystem ?? new FileSystem(), logFactory.CreateLogger());
+ private readonly ChangelogFileWriter _fileWriter = new(fileSystem ?? FileSystemFactory.RealRead, logFactory.CreateLogger());
private readonly ProductInferService _productInferService = new(
configurationContext.ProductsConfiguration);
diff --git a/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs b/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs
index 34f06d539a..cc92d1dc51 100644
--- a/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs
+++ b/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs
@@ -14,6 +14,7 @@
using Elastic.Documentation.ReleaseNotes;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Evaluation;
@@ -23,12 +24,12 @@ public class ChangelogPrEvaluationService(
IConfigurationContext configurationContext,
IGitHubPrService gitHubPrService,
ICoreService coreService,
- IFileSystem? fileSystem = null
+ ScopedFileSystem? fileSystem = null
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystem();
- private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem ?? new FileSystem());
+ private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
+ private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead);
public async Task EvaluatePr(IDiagnosticsCollector collector, EvaluatePrArguments input, Cancel ctx)
{
diff --git a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs
index 4d07ae3980..95d98324f6 100644
--- a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs
+++ b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs
@@ -15,6 +15,7 @@
using Elastic.Documentation.ReleaseNotes;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.GithubRelease;
@@ -68,13 +69,13 @@ public class GitHubReleaseChangelogService(
IConfigurationContext configurationContext,
IGitHubReleaseService? releaseService = null,
IGitHubPrService? prService = null,
- IFileSystem? fileSystem = null,
+ ScopedFileSystem? fileSystem = null,
ChangelogBundlingService? bundlingService = null
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystem();
- private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem ?? new FileSystem());
+ private readonly IFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealRead;
+ private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem ?? FileSystemFactory.RealRead);
private readonly IGitHubReleaseService _releaseService = releaseService ?? new GitHubReleaseService(logFactory);
private readonly IGitHubPrService _prService = prService ?? new GitHubPrService(logFactory);
private readonly ChangelogBundlingService _bundlingService = bundlingService ?? new ChangelogBundlingService(logFactory, configurationContext, fileSystem);
diff --git a/src/services/Elastic.Changelog/Rendering/ChangelogRenderer.cs b/src/services/Elastic.Changelog/Rendering/ChangelogRenderer.cs
index 19b55cb506..2f901c29d4 100644
--- a/src/services/Elastic.Changelog/Rendering/ChangelogRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/ChangelogRenderer.cs
@@ -6,13 +6,14 @@
using Elastic.Changelog.Rendering.Asciidoc;
using Elastic.Changelog.Rendering.Markdown;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Rendering;
///
/// Coordinates rendering of changelog output to different formats.
///
-public class ChangelogRenderer(IFileSystem fileSystem, ILogger logger)
+public class ChangelogRenderer(ScopedFileSystem fileSystem, ILogger logger)
{
///
/// Renders changelog output based on the specified file type.
diff --git a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs
index af1942494c..099ea5587b 100644
--- a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs
+++ b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs
@@ -14,6 +14,7 @@
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
using NetEscapades.EnumGenerators;
+using Nullean.ScopedFileSystem;
using YamlDotNet.Core;
namespace Elastic.Changelog.Rendering;
@@ -65,11 +66,11 @@ public enum ChangelogFileType
public class ChangelogRenderingService(
ILoggerFactory logFactory,
IConfigurationContext? configurationContext = null,
- IFileSystem? fileSystem = null
+ ScopedFileSystem? fileSystem = null
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
- private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystem();
+ private readonly ScopedFileSystem _fileSystem = fileSystem ?? FileSystemFactory.RealWrite;
public async Task RenderChangelogs(
IDiagnosticsCollector collector,
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs
index cdaf94fc43..7cace43f70 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs
@@ -6,6 +6,7 @@
using System.Text;
using Elastic.Documentation;
using Elastic.Documentation.ReleaseNotes;
+using Nullean.ScopedFileSystem;
using static System.Globalization.CultureInfo;
using static Elastic.Documentation.ChangelogEntryType;
@@ -14,7 +15,7 @@ namespace Elastic.Changelog.Rendering.Markdown;
///
/// Renderer for the breaking-changes.md changelog file
///
-public class BreakingChangesMarkdownRenderer(IFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
+public class BreakingChangesMarkdownRenderer(ScopedFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
{
///
public override string OutputFileName => "breaking-changes.md";
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogMarkdownRenderer.cs
index 2d512ace8d..c57d7b1162 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogMarkdownRenderer.cs
@@ -4,13 +4,14 @@
using System.Collections.Generic;
using System.IO.Abstractions;
+using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Rendering.Markdown;
///
/// Coordinates rendering of all markdown changelog files.
///
-public class ChangelogMarkdownRenderer(IFileSystem fileSystem)
+public class ChangelogMarkdownRenderer(ScopedFileSystem fileSystem)
{
///
/// Renders all markdown changelog files (index, breaking changes, deprecations, known issues, highlights).
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs
index 0a31bd1709..78dcf5fbd5 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs
@@ -5,6 +5,7 @@
using System.IO.Abstractions;
using System.Text;
using Elastic.Documentation.ReleaseNotes;
+using Nullean.ScopedFileSystem;
using static System.Globalization.CultureInfo;
using static Elastic.Documentation.ChangelogEntryType;
@@ -13,7 +14,7 @@ namespace Elastic.Changelog.Rendering.Markdown;
///
/// Renderer for the deprecations.md changelog file
///
-public class DeprecationsMarkdownRenderer(IFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
+public class DeprecationsMarkdownRenderer(ScopedFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
{
///
public override string OutputFileName => "deprecations.md";
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs
index f13394286f..f55a3c19a3 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs
@@ -5,6 +5,7 @@
using System.IO.Abstractions;
using System.Text;
using Elastic.Documentation.ReleaseNotes;
+using Nullean.ScopedFileSystem;
using static System.Globalization.CultureInfo;
namespace Elastic.Changelog.Rendering.Markdown;
@@ -12,7 +13,7 @@ namespace Elastic.Changelog.Rendering.Markdown;
///
/// Renderer for the highlights.md changelog file
///
-public class HighlightsMarkdownRenderer(IFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
+public class HighlightsMarkdownRenderer(ScopedFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
{
///
public override string OutputFileName => "highlights.md";
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs
index 84221cac0d..e00af54ca9 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs
@@ -6,6 +6,7 @@
using System.IO.Abstractions;
using System.Text;
using Elastic.Documentation.ReleaseNotes;
+using Nullean.ScopedFileSystem;
using static System.Globalization.CultureInfo;
using static Elastic.Documentation.ChangelogEntryType;
@@ -14,7 +15,7 @@ namespace Elastic.Changelog.Rendering.Markdown;
///
/// Renderer for the index.md changelog file containing features, enhancements, fixes, docs, regressions, and other changes
///
-public class IndexMarkdownRenderer(IFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
+public class IndexMarkdownRenderer(ScopedFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
{
///
public override string OutputFileName => "index.md";
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs
index bddadcf74d..a8640f14b3 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs
@@ -5,6 +5,7 @@
using System.IO.Abstractions;
using System.Text;
using Elastic.Documentation.ReleaseNotes;
+using Nullean.ScopedFileSystem;
using static System.Globalization.CultureInfo;
using static Elastic.Documentation.ChangelogEntryType;
@@ -13,7 +14,7 @@ namespace Elastic.Changelog.Rendering.Markdown;
///
/// Renderer for the known-issues.md changelog file
///
-public class KnownIssuesMarkdownRenderer(IFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
+public class KnownIssuesMarkdownRenderer(ScopedFileSystem fileSystem) : MarkdownRendererBase(fileSystem)
{
///
public override string OutputFileName => "known-issues.md";
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs b/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs
index 7c22e23532..6e7cc94473 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs
@@ -6,15 +6,16 @@
using System.IO.Abstractions;
using System.Text;
using Elastic.Documentation.ReleaseNotes;
+using Nullean.ScopedFileSystem;
namespace Elastic.Changelog.Rendering.Markdown;
///
/// Abstract base class for changelog markdown renderers
///
-public abstract class MarkdownRendererBase(IFileSystem fileSystem) : IChangelogMarkdownRenderer
+public abstract class MarkdownRendererBase(ScopedFileSystem fileSystem) : IChangelogMarkdownRenderer
{
- protected IFileSystem FileSystem { get; } = fileSystem;
+ protected ScopedFileSystem FileSystem { get; } = fileSystem;
///
public abstract string OutputFileName { get; }
diff --git a/src/services/Elastic.Documentation.Assembler/AssembleContext.cs b/src/services/Elastic.Documentation.Assembler/AssembleContext.cs
index 99ceba0b37..29ec966254 100644
--- a/src/services/Elastic.Documentation.Assembler/AssembleContext.cs
+++ b/src/services/Elastic.Documentation.Assembler/AssembleContext.cs
@@ -10,13 +10,14 @@
using Elastic.Documentation.Configuration.Search;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Diagnostics;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler;
public class AssembleContext : IDocumentationConfigurationContext
{
- public IFileSystem ReadFileSystem { get; }
- public IFileSystem WriteFileSystem { get; }
+ public ScopedFileSystem ReadFileSystem { get; }
+ public ScopedFileSystem WriteFileSystem { get; }
public IDiagnosticsCollector Collector { get; }
@@ -61,8 +62,8 @@ public AssembleContext(
IConfigurationContext configurationContext,
string environment,
IDiagnosticsCollector collector,
- IFileSystem readFileSystem,
- IFileSystem writeFileSystem,
+ ScopedFileSystem readFileSystem,
+ ScopedFileSystem writeFileSystem,
string? checkoutDirectory,
string? output
)
@@ -88,10 +89,12 @@ public AssembleContext(
Endpoints.Environment = environment;
var contentSource = Environment.ContentSource.ToStringFast(true);
- var defaultCheckoutDirectory = Path.Join(Paths.GitCommonRoot.FullName, ".artifacts", "checkouts", contentSource);
- CheckoutDirectory = ReadFileSystem.DirectoryInfo.New(checkoutDirectory ?? defaultCheckoutDirectory);
+ var defaultCheckoutDirectory = Path.Join(Paths.ApplicationData.FullName, "checkouts", contentSource);
+ CheckoutDirectory = checkoutDirectory is null
+ ? FileSystemFactory.AppData.DirectoryInfo.New(defaultCheckoutDirectory)
+ : ReadFileSystem.DirectoryInfo.New(checkoutDirectory);
var defaultOutputDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly");
- OutputDirectory = ReadFileSystem.DirectoryInfo.New(output ?? defaultOutputDirectory);
+ OutputDirectory = WriteFileSystem.DirectoryInfo.New(output ?? defaultOutputDirectory);
// Calculate the output directory with path prefix once
var pathPrefix = Environment.PathPrefix;
diff --git a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs
index 3e2e77e6be..5dc0898e04 100644
--- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs
@@ -15,6 +15,7 @@
using Elastic.Documentation.Navigation.Assembler;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.Building;
@@ -36,7 +37,8 @@ public async Task BuildAll(
bool? showHints,
IReadOnlySet? exporters,
bool? assumeBuild,
- FileSystem fs,
+ ScopedFileSystem readFs,
+ ScopedFileSystem writeFs,
Cancel ctx
)
{
@@ -56,7 +58,7 @@ Cancel ctx
_logger.LogInformation("Creating assemble context");
- var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, fs, fs, null, null);
+ var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, readFs, writeFs, null, null);
// --assume-build is not allowed on CI: it could serve stale content from a previous/cached build
// CI builds must always produce fresh, reproducible output
@@ -67,7 +69,7 @@ Cancel ctx
if (assumeBuild.GetValueOrDefault(false))
{
var indexHtmlPath = Path.Join(assembleContext.OutputDirectory.FullName, "docs", "index.html");
- if (assembleContext.OutputDirectory.Exists && fs.File.Exists(indexHtmlPath))
+ if (assembleContext.OutputDirectory.Exists && readFs.File.Exists(indexHtmlPath))
{
_logger.LogInformation("Assuming build already exists (--assume-build). Found index.html at {Path}. Skipping build.", indexHtmlPath);
return true;
@@ -100,7 +102,7 @@ Cancel ctx
var assembleSources = await AssembleSources.AssembleAsync(logFactory, assembleContext, checkouts, configurationContext, exporters, ctx);
var navigationFileInfo = configurationContext.ConfigurationFileProvider.NavigationFile;
- var siteNavigationFile = SiteNavigationFile.Deserialize(await fs.File.ReadAllTextAsync(navigationFileInfo.FullName, ctx));
+ var siteNavigationFile = SiteNavigationFile.Deserialize(await readFs.File.ReadAllTextAsync(navigationFileInfo.FullName, ctx));
var documentationSets = assembleSources.AssembleSets.Values.Select(s => s.DocumentationSet.Navigation).ToArray();
var navigation = new SiteNavigation(siteNavigationFile, assembleContext, documentationSets, assembleContext.Environment.PathPrefix);
@@ -122,7 +124,7 @@ Cancel ctx
await cloner.WriteLinkRegistrySnapshot(checkoutResult.LinkRegistrySnapshot, ctx);
var redirectsPath = Path.Join(assembleContext.OutputDirectory.FullName, "redirects.json");
- if (File.Exists(redirectsPath))
+ if (writeFs.File.Exists(redirectsPath))
await githubActionsService.SetOutputAsync("redirects-artifact-path", redirectsPath);
if (exporters.Contains(Exporter.Html))
diff --git a/src/services/Elastic.Documentation.Assembler/Building/AssemblerSitemapService.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerSitemapService.cs
index 8cd5be0f66..84843a2c37 100644
--- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerSitemapService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerSitemapService.cs
@@ -11,6 +11,7 @@
using Elastic.Documentation.Services;
using Elastic.Markdown.Exporters.Elasticsearch;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.Building;
@@ -25,7 +26,7 @@ ICoreService githubActionsService
public async Task GenerateSitemapAsync(
IDiagnosticsCollector collector,
- FileSystem fileSystem,
+ ScopedFileSystem fileSystem,
string? endpoint = null,
string? environment = null,
string? apiKey = null,
diff --git a/src/services/Elastic.Documentation.Assembler/Configuration/ConfigurationCloneService.cs b/src/services/Elastic.Documentation.Assembler/Configuration/ConfigurationCloneService.cs
index 621fecaf42..5dc7daf087 100644
--- a/src/services/Elastic.Documentation.Assembler/Configuration/ConfigurationCloneService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Configuration/ConfigurationCloneService.cs
@@ -15,7 +15,7 @@ namespace Elastic.Documentation.Assembler.Configuration;
public class ConfigurationCloneService(
ILoggerFactory logFactory,
AssemblyConfiguration assemblyConfiguration,
- FileSystem fs
+ IFileSystem fs
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
diff --git a/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryBuildMatchingService.cs b/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryBuildMatchingService.cs
index 2ca860f1ec..7e83293103 100644
--- a/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryBuildMatchingService.cs
+++ b/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryBuildMatchingService.cs
@@ -10,6 +10,7 @@
using Elastic.Documentation.LinkIndex;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.ContentSources;
@@ -18,7 +19,7 @@ public class RepositoryBuildMatchingService(
AssemblyConfiguration configuration,
IConfigurationContext configurationContext,
ICoreService githubActionsService,
- FileSystem fileSystem
+ ScopedFileSystem fileSystem
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
diff --git a/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryPublishValidationService.cs b/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryPublishValidationService.cs
index 2499b886ce..8f11b65b68 100644
--- a/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryPublishValidationService.cs
+++ b/src/services/Elastic.Documentation.Assembler/ContentSources/RepositoryPublishValidationService.cs
@@ -10,6 +10,7 @@
using Elastic.Documentation.LinkIndex;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.ContentSources;
@@ -17,7 +18,7 @@ public class RepositoryPublishValidationService(
ILoggerFactory logFactory,
AssemblyConfiguration configuration,
IConfigurationContext configurationContext,
- FileSystem fileSystem
+ ScopedFileSystem fileSystem
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
diff --git a/src/services/Elastic.Documentation.Assembler/Deploying/DeployUpdateRedirectsService.cs b/src/services/Elastic.Documentation.Assembler/Deploying/DeployUpdateRedirectsService.cs
index c1e5675ab3..01af135702 100644
--- a/src/services/Elastic.Documentation.Assembler/Deploying/DeployUpdateRedirectsService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Deploying/DeployUpdateRedirectsService.cs
@@ -12,7 +12,7 @@
namespace Elastic.Documentation.Assembler.Deploying;
-public class DeployUpdateRedirectsService(ILoggerFactory logFactory, FileSystem fileSystem) : IService
+public class DeployUpdateRedirectsService(ILoggerFactory logFactory, IFileSystem fileSystem) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
@@ -42,7 +42,7 @@ public async Task UpdateRedirects(
}
var kvsName = $"{kvsNamePrefix}-{environment}-redirects-kvs";
- var cloudFrontClient = new AwsCloudFrontKeyValueStoreProxy(collector, logFactory, fileSystem.DirectoryInfo.New(Directory.GetCurrentDirectory()));
+ var cloudFrontClient = new AwsCloudFrontKeyValueStoreProxy(collector, logFactory, fileSystem.DirectoryInfo.New(fileSystem.Directory.GetCurrentDirectory()));
cloudFrontClient.UpdateRedirects(kvsName, sourcedRedirects);
return collector.Errors == 0;
diff --git a/src/services/Elastic.Documentation.Assembler/Deploying/IncrementalDeployService.cs b/src/services/Elastic.Documentation.Assembler/Deploying/IncrementalDeployService.cs
index 52ac1160e9..6167412be6 100644
--- a/src/services/Elastic.Documentation.Assembler/Deploying/IncrementalDeployService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Deploying/IncrementalDeployService.cs
@@ -12,6 +12,7 @@
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.Deploying;
@@ -20,7 +21,7 @@ public class IncrementalDeployService(
AssemblyConfiguration assemblyConfiguration,
IConfigurationContext configurationContext,
ICoreService githubActionsService,
- FileSystem fileSystem
+ ScopedFileSystem fileSystem
) : IService
{
private readonly ILogger _logger = logFactory.CreateLogger();
@@ -50,7 +51,7 @@ public async Task Plan(IDiagnosticsCollector collector, string environment
if (!string.IsNullOrEmpty(@out))
{
var output = SyncPlan.Serialize(plan);
- await using var fileStream = new FileStream(@out, FileMode.Create, FileAccess.Write);
+ await using var fileStream = fileSystem.File.Create(@out);
await using var writer = new StreamWriter(fileStream);
await writer.WriteAsync(output);
_logger.LogInformation("Plan written to {OutputFile}", @out);
@@ -73,7 +74,7 @@ public async Task Apply(IDiagnosticsCollector collector, string environmen
collector.EmitError(planFile, "Plan file does not exist.");
return false;
}
- var planJson = await File.ReadAllTextAsync(planFile, ctx);
+ var planJson = await fileSystem.File.ReadAllTextAsync(planFile, ctx);
var plan = SyncPlan.Deserialize(planJson);
_logger.LogInformation("Remote listing completed: {RemoteListingCompleted}", plan.RemoteListingCompleted);
_logger.LogInformation("Total files to sync: {TotalFiles}", plan.TotalSyncRequests);
diff --git a/src/services/Elastic.Documentation.Assembler/Elastic.Documentation.Assembler.csproj b/src/services/Elastic.Documentation.Assembler/Elastic.Documentation.Assembler.csproj
index 175061e07c..82408eef33 100644
--- a/src/services/Elastic.Documentation.Assembler/Elastic.Documentation.Assembler.csproj
+++ b/src/services/Elastic.Documentation.Assembler/Elastic.Documentation.Assembler.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerIndexService.cs b/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerIndexService.cs
index 50d94e245f..7a3953704d 100644
--- a/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerIndexService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Indexing/AssemblerIndexService.cs
@@ -9,6 +9,7 @@
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
using static Elastic.Documentation.Exporter;
namespace Elastic.Documentation.Assembler.Indexing;
@@ -52,7 +53,8 @@ IEnvironmentVariables environmentVariables
///
///
public async Task Index(IDiagnosticsCollector collector,
- FileSystem fileSystem,
+ ScopedFileSystem readFs,
+ ScopedFileSystem writeFs,
string? endpoint = null,
string? environment = null,
string? apiKey = null,
@@ -105,10 +107,10 @@ public async Task Index(IDiagnosticsCollector collector,
CertificatePath = certificatePath,
CertificateNotRoot = certificateNotRoot
};
- await ElasticsearchEndpointConfigurator.ApplyAsync(cfg, options, collector, fileSystem, ctx);
+ await ElasticsearchEndpointConfigurator.ApplyAsync(cfg, options, collector, readFs, ctx);
var exporters = new HashSet { Elasticsearch };
- return await BuildAll(collector, strict: false, environment, metadataOnly: true, showHints: false, exporters, assumeBuild: false, fileSystem, ctx);
+ return await BuildAll(collector, strict: false, environment, metadataOnly: true, showHints: false, exporters, assumeBuild: false, readFs, writeFs, ctx);
}
}
diff --git a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs
index 5ec78d011b..56da04d68b 100644
--- a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs
@@ -9,6 +9,7 @@
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Services;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Elastic.Documentation.Assembler.Navigation;
@@ -16,7 +17,7 @@ public class GlobalNavigationService(
ILoggerFactory logFactory,
AssemblyConfiguration configuration,
IConfigurationContext configurationContext,
- IFileSystem fileSystem
+ ScopedFileSystem fileSystem
) : IService
{
public async Task Validate(IDiagnosticsCollector collector, Cancel ctx)
diff --git a/src/services/Elastic.Documentation.Assembler/Sourcing/AssemblerCloneService.cs b/src/services/Elastic.Documentation.Assembler/Sourcing/AssemblerCloneService.cs
index 92501c490f..f819ebe18a 100644
--- a/src/services/Elastic.Documentation.Assembler/Sourcing/AssemblerCloneService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Sourcing/AssemblerCloneService.cs
@@ -25,7 +25,7 @@ public async Task CloneAll(IDiagnosticsCollector collector, bool? strict,
var githubEnvironmentInput = githubActionsService.GetInput("environment");
environment ??= !string.IsNullOrEmpty(githubEnvironmentInput) ? githubEnvironmentInput : "dev";
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, environment, collector, fs, fs, null, null);
var cloner = new AssemblerRepositorySourcer(logFactory, assembleContext);
diff --git a/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs b/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs
index bff142bfe5..180e315b37 100644
--- a/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs
+++ b/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs
@@ -20,6 +20,7 @@
using Elastic.Markdown.IO;
using Elastic.Markdown.Page;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
using static System.StringComparison;
namespace Elastic.Documentation.Isolated;
@@ -43,7 +44,7 @@ public bool IsStrict(bool? strict)
public async Task Build(
IDiagnosticsCollector collector,
- IFileSystem fileSystem,
+ ScopedFileSystem fileSystem,
string? path = null,
string? output = null,
string? pathPrefix = null,
@@ -53,7 +54,7 @@ public async Task Build(
bool? metadataOnly = null,
IReadOnlySet? exporters = null,
string? canonicalBaseUrl = null,
- IFileSystem? writeFileSystem = null,
+ ScopedFileSystem? writeFileSystem = null,
bool skipOpenApi = false,
bool skipCrossLinks = false,
Cancel ctx = default
@@ -99,11 +100,15 @@ public async Task Build(
// At some point in the future we can remove this try catch
catch (Exception e) when (runningOnCi && e.Message.StartsWith("Can not locate docset.yml file in", OrdinalIgnoreCase))
{
+ // Derive the default output from `path` so it stays within the write FS scope.
+ // Using Paths.WorkingDirectoryRoot would be wrong when --path points to a different repo.
+ var rootFolder = !string.IsNullOrWhiteSpace(path) ? path : Paths.WorkingDirectoryRoot.FullName;
+ var writeFs = writeFileSystem ?? fileSystem;
var outputDirectory = !string.IsNullOrWhiteSpace(output)
- ? fileSystem.DirectoryInfo.New(output)
- : fileSystem.DirectoryInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts/docs/html"));
+ ? writeFs.DirectoryInfo.New(output)
+ : writeFs.DirectoryInfo.New(Path.Join(rootFolder, ".artifacts/docs/html"));
// we temporarily do not error when pointed to a non-documentation folder.
- _ = fileSystem.Directory.CreateDirectory(outputDirectory.FullName);
+ _ = writeFs.Directory.CreateDirectory(outputDirectory.FullName);
_logger.LogInformation("Skipping build as we are running on a merge commit and the docs folder is out of date and has no docset.yml. {Message}",
e.Message);
@@ -124,7 +129,7 @@ public async Task Build(
else
{
using var codexReader = context.Configuration.Registry != DocSetRegistry.Public
- ? new GitLinkIndexReader(context.Configuration.Registry.ToStringFast(true), fileSystem)
+ ? new GitLinkIndexReader(context.Configuration.Registry.ToStringFast(true), FileSystemFactory.AppData)
: null;
var crossLinkFetcher = new DocSetConfigurationCrossLinkFetcher(
diff --git a/src/services/Elastic.Documentation.Isolated/IsolatedIndexService.cs b/src/services/Elastic.Documentation.Isolated/IsolatedIndexService.cs
index 0f710c3fd8..b20980e233 100644
--- a/src/services/Elastic.Documentation.Isolated/IsolatedIndexService.cs
+++ b/src/services/Elastic.Documentation.Isolated/IsolatedIndexService.cs
@@ -7,6 +7,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
using static Elastic.Documentation.Exporter;
namespace Elastic.Documentation.Isolated;
@@ -49,7 +50,7 @@ IEnvironmentVariables environmentVariables
///
///
public async Task Index(IDiagnosticsCollector collector,
- FileSystem fileSystem,
+ ScopedFileSystem fileSystem,
string? path = null,
string? endpoint = null,
string? apiKey = null,
diff --git a/src/tooling/docs-builder/Commands/Assembler/AssemblerCommands.cs b/src/tooling/docs-builder/Commands/Assembler/AssemblerCommands.cs
index 831eb5434c..5c3cf9f25b 100644
--- a/src/tooling/docs-builder/Commands/Assembler/AssemblerCommands.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/AssemblerCommands.cs
@@ -63,10 +63,11 @@ static async (s, collector, state, ctx) => await s.CloneAll(collector, state.str
);
var buildService = new AssemblerBuildService(logFactory, assemblyConfiguration, configurationContext, githubActionsService, environmentVariables);
- var fs = new FileSystem();
- serviceInvoker.AddCommand(buildService, (strict, environment, metadataOnly, showHints, exporters, assumeBuild, fs), strict ?? false,
+ var readFs = FileSystemFactory.RealRead;
+ var writeFs = FileSystemFactory.RealWrite;
+ serviceInvoker.AddCommand(buildService, (strict, environment, metadataOnly, showHints, exporters, assumeBuild, readFs, writeFs), strict ?? false,
static async (s, collector, state, ctx) =>
- await s.BuildAll(collector, state.strict, state.environment, state.metadataOnly, state.showHints, state.exporters, state.assumeBuild, state.fs, ctx)
+ await s.BuildAll(collector, state.strict, state.environment, state.metadataOnly, state.showHints, state.exporters, state.assumeBuild, state.readFs, state.writeFs, ctx)
);
var result = await serviceInvoker.InvokeAsync(ctx);
@@ -141,11 +142,12 @@ public async Task BuildAll(
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var readFs = FileSystemFactory.RealRead;
+ var writeFs = FileSystemFactory.RealWrite;
var service = new AssemblerBuildService(logFactory, assemblyConfiguration, configurationContext, githubActionsService, environmentVariables);
- serviceInvoker.AddCommand(service, (strict, environment, assumeBuild, metadataOnly, showHints, exporters, fs), strict ?? false,
+ serviceInvoker.AddCommand(service, (strict, environment, assumeBuild, metadataOnly, showHints, exporters, readFs, writeFs), strict ?? false,
static async (s, collector, state, ctx) =>
- await s.BuildAll(collector, state.strict, state.environment, state.metadataOnly, state.showHints, state.exporters, state.assumeBuild, state.fs, ctx)
+ await s.BuildAll(collector, state.strict, state.environment, state.metadataOnly, state.showHints, state.exporters, state.assumeBuild, state.readFs, state.writeFs, ctx)
);
return await serviceInvoker.InvokeAsync(ctx);
diff --git a/src/tooling/docs-builder/Commands/Assembler/AssemblerIndexCommand.cs b/src/tooling/docs-builder/Commands/Assembler/AssemblerIndexCommand.cs
index 21470e3bc3..7a8a5a9316 100644
--- a/src/tooling/docs-builder/Commands/Assembler/AssemblerIndexCommand.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/AssemblerIndexCommand.cs
@@ -88,9 +88,10 @@ public async Task Index(
)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var readFs = FileSystemFactory.RealRead;
+ var writeFs = FileSystemFactory.RealWrite;
var service = new AssemblerIndexService(logFactory, configuration, configurationContext, githubActionsService, environmentVariables);
- var state = (fs,
+ var state = (readFs, writeFs,
// endpoint options
endpoint, environment, apiKey, username, password,
// inference options
@@ -103,7 +104,7 @@ public async Task Index(
disableSslVerification, certificateFingerprint, certificatePath, certificateNotRoot
);
serviceInvoker.AddCommand(service, state,
- static async (s, collector, state, ctx) => await s.Index(collector, state.fs,
+ static async (s, collector, state, ctx) => await s.Index(collector, state.readFs, state.writeFs,
// endpoint options
state.endpoint, state.environment, state.apiKey, state.username, state.password,
// inference options
diff --git a/src/tooling/docs-builder/Commands/Assembler/AssemblerSitemapCommand.cs b/src/tooling/docs-builder/Commands/Assembler/AssemblerSitemapCommand.cs
index eefc396f46..da074c606a 100644
--- a/src/tooling/docs-builder/Commands/Assembler/AssemblerSitemapCommand.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/AssemblerSitemapCommand.cs
@@ -59,7 +59,7 @@ public async Task Sitemap(
)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealWrite;
var service = new AssemblerSitemapService(logFactory, configuration, configurationContext, githubActionsService);
var state = (fs,
endpoint, environment, apiKey, username, password,
diff --git a/src/tooling/docs-builder/Commands/Assembler/ConfigurationCommands.cs b/src/tooling/docs-builder/Commands/Assembler/ConfigurationCommands.cs
index bb6c9f8602..0cea37f2cb 100644
--- a/src/tooling/docs-builder/Commands/Assembler/ConfigurationCommands.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/ConfigurationCommands.cs
@@ -28,7 +28,7 @@ public async Task CloneConfigurationFolder(string? gitRef = null, bool loca
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var service = new ConfigurationCloneService(logFactory, assemblyConfiguration, fs);
serviceInvoker.AddCommand(service, (gitRef, local), static async (s, collector, state, ctx) =>
await s.InitConfigurationToApplicationData(collector, state.gitRef, state.local, ctx));
diff --git a/src/tooling/docs-builder/Commands/Assembler/ContentSourceCommands.cs b/src/tooling/docs-builder/Commands/Assembler/ContentSourceCommands.cs
index f602232a00..21080a6fac 100644
--- a/src/tooling/docs-builder/Commands/Assembler/ContentSourceCommands.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/ContentSourceCommands.cs
@@ -27,7 +27,7 @@ public async Task Validate(Cancel ctx = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var service = new RepositoryPublishValidationService(logFactory, configuration, configurationContext, fs);
serviceInvoker.AddCommand(service, static async (s, collector, ctx) => await s.ValidatePublishStatus(collector, ctx));
@@ -43,7 +43,7 @@ public async Task Match([Argument] string? repository = null, [Argument] st
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var service = new RepositoryBuildMatchingService(logFactory, configuration, configurationContext, githubActionsService, fs);
serviceInvoker.AddCommand(service, (repository, branchOrTag),
static async (s, collector, state, ctx) =>
diff --git a/src/tooling/docs-builder/Commands/Assembler/DeployCommands.cs b/src/tooling/docs-builder/Commands/Assembler/DeployCommands.cs
index c6a6d40644..daf4eec188 100644
--- a/src/tooling/docs-builder/Commands/Assembler/DeployCommands.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/DeployCommands.cs
@@ -33,7 +33,7 @@ public async Task Plan(string environment, string s3BucketName, string @out
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var service = new IncrementalDeployService(logFactory, assemblyConfiguration, configurationContext, githubActionsService, fs);
serviceInvoker.AddCommand(service, (environment, s3BucketName, @out, deleteThreshold),
static async (s, collector, state, ctx) => await s.Plan(collector, state.environment, state.s3BucketName, state.@out, state.deleteThreshold, ctx)
@@ -51,7 +51,7 @@ public async Task Apply(string environment, string s3BucketName, string pla
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var service = new IncrementalDeployService(logFactory, assemblyConfiguration, configurationContext, githubActionsService, fs);
serviceInvoker.AddCommand(service, (environment, s3BucketName, planFile),
static async (s, collector, state, ctx) => await s.Apply(collector, state.environment, state.s3BucketName, state.planFile, ctx)
@@ -68,7 +68,7 @@ public async Task UpdateRedirects(string environment, string? redirectsFile
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var service = new DeployUpdateRedirectsService(logFactory, fs);
serviceInvoker.AddCommand(service, (environment, redirectsFile),
static async (s, collector, state, ctx) => await s.UpdateRedirects(collector, state.environment, state.redirectsFile, ctx: ctx)
diff --git a/src/tooling/docs-builder/Commands/Assembler/NavigationCommands.cs b/src/tooling/docs-builder/Commands/Assembler/NavigationCommands.cs
index 59df5cd89a..d6ca3075f9 100644
--- a/src/tooling/docs-builder/Commands/Assembler/NavigationCommands.cs
+++ b/src/tooling/docs-builder/Commands/Assembler/NavigationCommands.cs
@@ -26,7 +26,7 @@ IConfigurationContext configurationContext
public async Task Validate(Cancel ctx = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var service = new GlobalNavigationService(logFactory, configuration, configurationContext, new FileSystem());
+ var service = new GlobalNavigationService(logFactory, configuration, configurationContext, FileSystemFactory.RealRead);
serviceInvoker.AddCommand(service, static async (s, collector, ctx) => await s.Validate(collector, ctx));
return await serviceInvoker.InvokeAsync(ctx);
}
@@ -38,7 +38,7 @@ public async Task Validate(Cancel ctx = default)
public async Task ValidateLocalLinkReference([Argument] string? file = null, Cancel ctx = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var service = new GlobalNavigationService(logFactory, configuration, configurationContext, new FileSystem());
+ var service = new GlobalNavigationService(logFactory, configuration, configurationContext, FileSystemFactory.RealRead);
serviceInvoker.AddCommand(service, file, static async (s, collector, file, ctx) => await s.ValidateLocalLinkReference(collector, file, ctx));
return await serviceInvoker.InvokeAsync(ctx);
}
diff --git a/src/tooling/docs-builder/Commands/ChangelogCommand.cs b/src/tooling/docs-builder/Commands/ChangelogCommand.cs
index afb3180d3f..28b52241af 100644
--- a/src/tooling/docs-builder/Commands/ChangelogCommand.cs
+++ b/src/tooling/docs-builder/Commands/ChangelogCommand.cs
@@ -39,7 +39,7 @@ IEnvironmentVariables environmentVariables
[GeneratedRegex(@"^( *output_directory:\s*).+$", RegexOptions.Multiline)]
private static partial Regex BundleOutputDirectoryRegex();
- private readonly IFileSystem _fileSystem = new FileSystem();
+ private readonly IFileSystem _fileSystem = FileSystemFactory.RealRead;
private readonly ILogger _logger = logFactory.CreateLogger();
///
/// Changelog commands. Use 'changelog add' to create a new changelog or 'changelog bundle' to create a consolidated list of changelogs.
@@ -91,7 +91,7 @@ public Task Init(
var useNonDefaultChangelogDir = changelogDir != null;
var useNonDefaultBundlesDir = bundlesDir != null;
- var repoRoot = Paths.DetermineSourceDirectoryRoot(docsFolder)?.FullName ?? docsFolder.FullName;
+ var repoRoot = Paths.FindGitRoot(docsFolder)?.FullName ?? docsFolder.FullName;
// Create changelog.yml from example if it does not exist
if (!_fileSystem.File.Exists(configPath))
@@ -288,7 +288,7 @@ public async Task Create(
// Load changelog config and apply fallbacks for all modes.
// Precedence: CLI option > bundle section in changelog.yml > built-in default.
// This applies to --prs, --issues, and --release-version alike.
- var bundleConfig = await new ChangelogConfigurationLoader(logFactory, configurationContext, new System.IO.Abstractions.FileSystem())
+ var bundleConfig = await new ChangelogConfigurationLoader(logFactory, configurationContext, _fileSystem)
.LoadChangelogConfiguration(collector, config, ctx);
var resolvedRepo = !string.IsNullOrWhiteSpace(repo) ? repo : bundleConfig?.Bundle?.Repo;
var resolvedOwner = owner ?? bundleConfig?.Bundle?.Owner ?? "elastic";
@@ -546,7 +546,7 @@ public async Task Bundle(
}
// Precedence: --repo CLI > bundle.repo config; --owner CLI > bundle.owner config > "elastic"
- var bundleConfig = await new ChangelogConfigurationLoader(logFactory, configurationContext, new System.IO.Abstractions.FileSystem())
+ var bundleConfig = await new ChangelogConfigurationLoader(logFactory, configurationContext, _fileSystem)
.LoadChangelogConfiguration(collector, config, ctx);
var resolvedRepo = !string.IsNullOrWhiteSpace(repo) ? repo : bundleConfig?.Bundle?.Repo;
var resolvedOwner = owner ?? bundleConfig?.Bundle?.Owner ?? "elastic";
@@ -855,7 +855,7 @@ public async Task Remove(
}
// Precedence: --repo CLI > bundle.repo config; --owner CLI > bundle.owner config > "elastic"
- var bundleConfig = await new ChangelogConfigurationLoader(logFactory, configurationContext, new System.IO.Abstractions.FileSystem())
+ var bundleConfig = await new ChangelogConfigurationLoader(logFactory, configurationContext, _fileSystem)
.LoadChangelogConfiguration(collector, config, ctx);
var resolvedRepo = !string.IsNullOrWhiteSpace(repo) ? repo : bundleConfig?.Bundle?.Repo;
var resolvedOwner = owner ?? bundleConfig?.Bundle?.Owner ?? "elastic";
@@ -1114,7 +1114,7 @@ public async Task GitHubRelease(
await using var serviceInvoker = new ServiceInvoker(collector);
// --output CLI > bundle.directory config > ./changelogs (service default)
- var bundleConfig = await new ChangelogConfigurationLoader(logFactory, configurationContext, new System.IO.Abstractions.FileSystem())
+ var bundleConfig = await new ChangelogConfigurationLoader(logFactory, configurationContext, _fileSystem)
.LoadChangelogConfiguration(collector, config, ctx);
var resolvedOutput = !string.IsNullOrWhiteSpace(output) ? output : bundleConfig?.Bundle?.Directory;
diff --git a/src/tooling/docs-builder/Commands/Codex/CodexCommands.cs b/src/tooling/docs-builder/Commands/Codex/CodexCommands.cs
index 6f736a25aa..a5d62a18cb 100644
--- a/src/tooling/docs-builder/Commands/Codex/CodexCommands.cs
+++ b/src/tooling/docs-builder/Commands/Codex/CodexCommands.cs
@@ -51,7 +51,7 @@ public async Task CloneAndBuild(
Cancel ctx = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
// Load codex configuration
var configPath = fs.Path.GetFullPath(config);
@@ -125,7 +125,7 @@ public async Task Clone(
Cancel ctx = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var configPath = fs.Path.GetFullPath(config);
var configFile = fs.FileInfo.New(configPath);
@@ -173,7 +173,7 @@ public async Task Build(
Cancel ctx = default)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var configPath = fs.Path.GetFullPath(config);
var configFile = fs.FileInfo.New(configPath);
@@ -226,7 +226,7 @@ public async Task Serve(
string? path = null,
Cancel ctx = default)
{
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var servePath = path ?? fs.Path.Join(
Environment.CurrentDirectory, ".artifacts", "codex", "docs");
diff --git a/src/tooling/docs-builder/Commands/Codex/CodexIndexCommand.cs b/src/tooling/docs-builder/Commands/Codex/CodexIndexCommand.cs
index c3e638b836..65d2b08ea9 100644
--- a/src/tooling/docs-builder/Commands/Codex/CodexIndexCommand.cs
+++ b/src/tooling/docs-builder/Commands/Codex/CodexIndexCommand.cs
@@ -94,7 +94,7 @@ public async Task Index(
)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var configPath = fs.Path.GetFullPath(config);
var configFile = fs.FileInfo.New(configPath);
diff --git a/src/tooling/docs-builder/Commands/Codex/CodexUpdateRedirectsCommand.cs b/src/tooling/docs-builder/Commands/Codex/CodexUpdateRedirectsCommand.cs
index 3b8e3ec3d5..d888afcfc7 100644
--- a/src/tooling/docs-builder/Commands/Codex/CodexUpdateRedirectsCommand.cs
+++ b/src/tooling/docs-builder/Commands/Codex/CodexUpdateRedirectsCommand.cs
@@ -5,6 +5,7 @@
using System.IO.Abstractions;
using ConsoleAppFramework;
using Elastic.Documentation.Assembler.Deploying;
+using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Codex;
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Services;
@@ -34,7 +35,7 @@ public async Task Run(
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealRead;
var configPath = fs.Path.GetFullPath(config);
var configFile = fs.FileInfo.New(configPath);
diff --git a/src/tooling/docs-builder/Commands/DiffCommands.cs b/src/tooling/docs-builder/Commands/DiffCommands.cs
index 895040cf8e..c3d1d01097 100644
--- a/src/tooling/docs-builder/Commands/DiffCommands.cs
+++ b/src/tooling/docs-builder/Commands/DiffCommands.cs
@@ -29,7 +29,7 @@ public async Task ValidateRedirects(string? path = null, Cancel ctx = defau
await using var serviceInvoker = new ServiceInvoker(collector);
var service = new LocalChangeTrackingService(logFactory, configurationContext);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealGitRootForPath(path);
serviceInvoker.AddCommand(service, (path, fs),
async static (s, collector, state, _) => await s.ValidateRedirects(collector, state.path, state.fs)
diff --git a/src/tooling/docs-builder/Commands/FormatCommand.cs b/src/tooling/docs-builder/Commands/FormatCommand.cs
index 7672c74583..2849bd3901 100644
--- a/src/tooling/docs-builder/Commands/FormatCommand.cs
+++ b/src/tooling/docs-builder/Commands/FormatCommand.cs
@@ -43,7 +43,7 @@ public async Task Format(
await using var serviceInvoker = new ServiceInvoker(collector);
var service = new FormatService(logFactory, configurationContext);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealGitRootForPath(path);
serviceInvoker.AddCommand(service, (path, check, fs),
async static (s, collector, state, ctx) => await s.Format(collector, state.path, state.check, state.fs, ctx)
diff --git a/src/tooling/docs-builder/Commands/InboundLinkCommands.cs b/src/tooling/docs-builder/Commands/InboundLinkCommands.cs
index 65e20a419d..1bdc3220a7 100644
--- a/src/tooling/docs-builder/Commands/InboundLinkCommands.cs
+++ b/src/tooling/docs-builder/Commands/InboundLinkCommands.cs
@@ -4,6 +4,7 @@
using System.IO.Abstractions;
using ConsoleAppFramework;
+using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Links.InboundLinks;
using Elastic.Documentation.Services;
@@ -13,7 +14,7 @@ namespace Documentation.Builder.Commands;
internal sealed class InboundLinkCommands(ILoggerFactory logFactory, IDiagnosticsCollector collector)
{
- private readonly LinkIndexService _linkIndexService = new(logFactory, new FileSystem());
+ private readonly LinkIndexService _linkIndexService = new(logFactory, FileSystemFactory.RealRead);
/// Validate all published cross_links in all published links.json files.
///
diff --git a/src/tooling/docs-builder/Commands/IndexCommand.cs b/src/tooling/docs-builder/Commands/IndexCommand.cs
index d7c27ea86b..556489eb0f 100644
--- a/src/tooling/docs-builder/Commands/IndexCommand.cs
+++ b/src/tooling/docs-builder/Commands/IndexCommand.cs
@@ -86,7 +86,7 @@ public async Task Index(
)
{
await using var serviceInvoker = new ServiceInvoker(collector);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealGitRootForPath(path);
var service = new IsolatedIndexService(logFactory, configurationContext, githubActionsService, environmentVariables);
var state = (fs, path,
// endpoint options
diff --git a/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs b/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs
index 4b84218ec3..a1474e3505 100644
--- a/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs
+++ b/src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs
@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information
using System.IO.Abstractions;
-using System.IO.Abstractions.TestingHelpers;
using Actions.Core.Services;
using ConsoleAppFramework;
using Documentation.Builder.Arguments;
@@ -63,15 +62,17 @@ public async Task Build(
await using var serviceInvoker = new ServiceInvoker(collector);
var service = new IsolatedBuildService(logFactory, configurationContext, githubActionsService, environmentVariables);
- IFileSystem fs = inMemory ? new MockFileSystem() : new FileSystem();
+ var readFs = inMemory ? FileSystemFactory.InMemory() : FileSystemFactory.RealGitRootForPath(path);
+ // For real builds supply an explicit write FS without .git access; for in-memory null falls back to readFs
+ var writeFs = inMemory ? null : FileSystemFactory.RealGitRootForPathWrite(path, output);
var strictCommand = service.IsStrict(strict);
serviceInvoker.AddCommand(service,
- (path, output, pathPrefix, force, strict, allowIndexing, metadataOnly, exporters, canonicalBaseUrl, fs, skipApi), strictCommand,
+ (path, output, pathPrefix, force, strict, allowIndexing, metadataOnly, exporters, canonicalBaseUrl, readFs, writeFs, skipApi), strictCommand,
async static (s, collector, state, ctx) => await s.Build(
- collector, state.fs, state.path, state.output, state.pathPrefix,
+ collector, state.readFs, state.path, state.output, state.pathPrefix,
state.force, state.strict, state.allowIndexing, state.metadataOnly,
- state.exporters, state.canonicalBaseUrl, null, state.skipApi, false, ctx
+ state.exporters, state.canonicalBaseUrl, state.writeFs, state.skipApi, false, ctx
)
);
return await serviceInvoker.InvokeAsync(ctx);
diff --git a/src/tooling/docs-builder/Commands/MoveCommand.cs b/src/tooling/docs-builder/Commands/MoveCommand.cs
index 2ce50e6afd..540e7da3a7 100644
--- a/src/tooling/docs-builder/Commands/MoveCommand.cs
+++ b/src/tooling/docs-builder/Commands/MoveCommand.cs
@@ -38,7 +38,7 @@ public async Task Move(
await using var serviceInvoker = new ServiceInvoker(collector);
var service = new MoveFileService(logFactory, configurationContext);
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealGitRootForPath(path);
serviceInvoker.AddCommand(service, (source, target, dryRun, path, fs),
async static (s, collector, state, ctx) => await s.Move(collector, state.source, state.target, state.dryRun, state.path, state.fs, ctx)
diff --git a/src/tooling/docs-builder/Commands/ServeCommand.cs b/src/tooling/docs-builder/Commands/ServeCommand.cs
index afc2b1e066..6b3b3ba883 100644
--- a/src/tooling/docs-builder/Commands/ServeCommand.cs
+++ b/src/tooling/docs-builder/Commands/ServeCommand.cs
@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information
using System.IO.Abstractions;
-using System.IO.Abstractions.TestingHelpers;
using ConsoleAppFramework;
using Documentation.Builder.Http;
using Elastic.Documentation.Configuration;
@@ -28,7 +27,7 @@ internal sealed class ServeCommand(ILoggerFactory logFactory, IConfigurationCont
[Command("")]
public async Task Serve(string? path = null, int port = 3000, bool watch = false, Cancel ctx = default)
{
- var host = new DocumentationWebHost(logFactory, path, port, new FileSystem(), new MockFileSystem(), configurationContext, watch);
+ var host = new DocumentationWebHost(logFactory, path, port, FileSystemFactory.RealGitRootForPath(path), FileSystemFactory.InMemory(), configurationContext, watch);
await host.RunAsync(ctx);
_logger.LogInformation("Find your documentation at http://localhost:{Port}/{Path}", port,
host.GeneratorState.Generator.DocumentationSet.FirstInterestingUrl.TrimStart('/')
diff --git a/src/tooling/docs-builder/Filters/CheckForUpdatesFilter.cs b/src/tooling/docs-builder/Filters/CheckForUpdatesFilter.cs
index 0a246d3090..23eb693ad2 100644
--- a/src/tooling/docs-builder/Filters/CheckForUpdatesFilter.cs
+++ b/src/tooling/docs-builder/Filters/CheckForUpdatesFilter.cs
@@ -2,6 +2,7 @@
// 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;
using System.Reflection;
using ConsoleAppFramework;
using Elastic.Documentation;
@@ -11,7 +12,9 @@ namespace Documentation.Builder.Filters;
internal sealed class CheckForUpdatesFilter(ConsoleAppFilter next, GlobalCliArgs cli) : ConsoleAppFilter(next)
{
- private readonly FileInfo _stateFile = new(Path.Join(Paths.ApplicationData.FullName, "docs-build-check.state"));
+ // Only accesses ApplicationData — no workspace access needed
+ private static readonly IFileSystem Fs = FileSystemFactory.AppData;
+ private readonly IFileInfo _stateFile = Fs.FileInfo.New(Path.Join(Paths.ApplicationData.FullName, "docs-build-check.state"));
public override async Task InvokeAsync(ConsoleAppContext context, Cancel ctx)
{
@@ -63,7 +66,7 @@ private static void CompareWithAssemblyVersion(Uri latestVersionUrl)
// only check for new versions once per hour
if (_stateFile.Exists && _stateFile.LastWriteTimeUtc >= DateTime.UtcNow.Subtract(TimeSpan.FromHours(1)))
{
- var url = await File.ReadAllTextAsync(_stateFile.FullName, ctx);
+ var url = await Fs.File.ReadAllTextAsync(_stateFile.FullName, ctx);
if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
return uri;
}
@@ -76,9 +79,9 @@ private static void CompareWithAssemblyVersion(Uri latestVersionUrl)
if (redirectUrl is not null && _stateFile.Directory is not null)
{
// ensure the 'elastic' folder exists.
- if (!Directory.Exists(_stateFile.Directory.FullName))
- _ = Directory.CreateDirectory(_stateFile.Directory.FullName);
- await File.WriteAllTextAsync(_stateFile.FullName, redirectUrl.ToString(), ctx);
+ if (!Fs.Directory.Exists(_stateFile.Directory.FullName))
+ _ = Fs.Directory.CreateDirectory(_stateFile.Directory.FullName);
+ await Fs.File.WriteAllTextAsync(_stateFile.FullName, redirectUrl.ToString(), ctx);
}
return redirectUrl;
}
diff --git a/src/tooling/docs-builder/Http/DocumentationWebHost.cs b/src/tooling/docs-builder/Http/DocumentationWebHost.cs
index d85303c585..9fe23eda14 100644
--- a/src/tooling/docs-builder/Http/DocumentationWebHost.cs
+++ b/src/tooling/docs-builder/Http/DocumentationWebHost.cs
@@ -4,6 +4,7 @@
using System.IO.Abstractions;
using System.Net;
+using Nullean.ScopedFileSystem;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
@@ -34,15 +35,15 @@ public class DocumentationWebHost
private readonly WebApplication _webApplication;
private readonly IHostedService _hostedService;
- private readonly IFileSystem _writeFileSystem;
+ private readonly ScopedFileSystem _writeFileSystem;
public InMemoryBuildState InMemoryBuildState { get; }
public DocumentationWebHost(ILoggerFactory logFactory,
string? path,
int port,
- IFileSystem readFs,
- IFileSystem writeFs,
+ ScopedFileSystem readFs,
+ ScopedFileSystem writeFs,
IConfigurationContext configurationContext,
bool isWatchBuild
)
diff --git a/src/tooling/docs-builder/Http/InMemoryBuildState.cs b/src/tooling/docs-builder/Http/InMemoryBuildState.cs
index cced222bc1..43154396c9 100644
--- a/src/tooling/docs-builder/Http/InMemoryBuildState.cs
+++ b/src/tooling/docs-builder/Http/InMemoryBuildState.cs
@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information
using System.IO.Abstractions;
-using System.IO.Abstractions.TestingHelpers;
using System.Text.Json.Serialization;
using System.Threading.Channels;
using Actions.Core;
@@ -15,6 +14,7 @@
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.Isolated;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
namespace Documentation.Builder.Http;
@@ -54,7 +54,7 @@ public class InMemoryBuildState(ILoggerFactory loggerFactory, IConfigurationCont
private readonly List _diagnostics = [];
// Reuse MockFileSystem across builds to benefit from caching
- private readonly MockFileSystem _writeFs = new();
+ private readonly ScopedFileSystem _writeFs = FileSystemFactory.InMemory();
// Broadcast: maintain list of connected client channels
private readonly Lock _clientsLock = new();
@@ -169,7 +169,7 @@ private async Task ExecuteBuildAsync(string sourcePath, Cancel ct)
// Create a diagnostics collector that streams to our channel
var streamingCollector = new StreamingDiagnosticsCollector(_loggerFactory, this);
- var readFs = new FileSystem();
+ var readFs = FileSystemFactory.RealGitRootForPath(sourcePath);
var service = new IsolatedBuildService(_loggerFactory, _configurationContext, new NullCoreService(), SystemEnvironmentVariables.Instance);
_logger.LogInformation("Starting in-memory validation build for {Path}", sourcePath);
diff --git a/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs b/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs
index 5f9ac5a992..104e0aa740 100644
--- a/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs
+++ b/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs
@@ -45,7 +45,7 @@ bool isWatchBuild
ApiPath = context.WriteFileSystem.DirectoryInfo.New(Path.Join(outputPath.FullName, "api"));
if (context.Configuration.Registry != DocSetRegistry.Public)
- _codexReader = new GitLinkIndexReader(context.Configuration.Registry.ToStringFast(true), context.ReadFileSystem);
+ _codexReader = new GitLinkIndexReader(context.Configuration.Registry.ToStringFast(true), FileSystemFactory.AppData);
_crossLinkFetcher = new DocSetConfigurationCrossLinkFetcher(logFactory, _context.Configuration, codexLinkIndexReader: _codexReader);
// we pass NoopCrossLinkResolver.Instance here because `ReloadAsync` will always be called when the is started.
diff --git a/src/tooling/docs-builder/Http/StaticWebHost.cs b/src/tooling/docs-builder/Http/StaticWebHost.cs
index 9aa420920b..788509e6bc 100644
--- a/src/tooling/docs-builder/Http/StaticWebHost.cs
+++ b/src/tooling/docs-builder/Http/StaticWebHost.cs
@@ -25,7 +25,7 @@ public class StaticWebHost
public StaticWebHost(int port, string? path)
{
_contentRoot = path ?? Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly");
- var fs = new FileSystem();
+ var fs = FileSystemFactory.RealGitRootForPath(_contentRoot);
var dir = fs.DirectoryInfo.New(_contentRoot);
if (!dir.Exists)
throw new Exception($"Can not serve empty directory: {_contentRoot}");
diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/AssemblerConfigurationTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/AssemblerConfigurationTests.cs
index 9b0581b53c..61775c3e74 100644
--- a/tests-integration/Elastic.Assembler.IntegrationTests/AssemblerConfigurationTests.cs
+++ b/tests-integration/Elastic.Assembler.IntegrationTests/AssemblerConfigurationTests.cs
@@ -9,6 +9,7 @@
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Diagnostics;
using Microsoft.Extensions.Logging.Abstractions;
+using Nullean.ScopedFileSystem;
namespace Elastic.Assembler.IntegrationTests;
@@ -28,7 +29,8 @@ public PublicOnlyAssemblerConfigurationTests()
var configurationFileProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, FileSystem, skipPrivateRepositories: true);
var configurationContext = TestHelpers.CreateConfigurationContext(FileSystem, configurationFileProvider: configurationFileProvider);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- Context = new AssembleContext(config, configurationContext, "dev", Collector, FileSystem, FileSystem, CheckoutDirectory.FullName, null);
+ var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem);
+ Context = new AssembleContext(config, configurationContext, "dev", Collector, scopedFs, scopedFs, CheckoutDirectory.FullName, null);
}
[Fact]
@@ -64,7 +66,8 @@ public AssemblerConfigurationTests(DocumentationFixture fixture, ITestOutputHelp
Collector = new DiagnosticsCollector([]);
var configurationContext = TestHelpers.CreateConfigurationContext(FileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- Context = new AssembleContext(config, configurationContext, "dev", Collector, FileSystem, FileSystem, CheckoutDirectory.FullName, null);
+ var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem);
+ Context = new AssembleContext(config, configurationContext, "dev", Collector, scopedFs, scopedFs, CheckoutDirectory.FullName, null);
}
[Fact]
diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/DocsSyncTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/DocsSyncTests.cs
index 4b6d0750db..cb8525a4d6 100644
--- a/tests-integration/Elastic.Assembler.IntegrationTests/DocsSyncTests.cs
+++ b/tests-integration/Elastic.Assembler.IntegrationTests/DocsSyncTests.cs
@@ -16,6 +16,7 @@
using Elastic.Documentation.ServiceDefaults.Telemetry;
using FakeItEasy;
using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
using OpenTelemetry;
using OpenTelemetry.Trace;
@@ -39,12 +40,14 @@ public async Task TestPlan()
{ "docs/update.md", new MockFileData("# Existing Document") },
}, new MockFileSystemOptions
{
- CurrentDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly")
+ CurrentDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"),
});
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- var context = new AssembleContext(config, configurationContext, "dev", collector, fileSystem, fileSystem, null, Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"));
+ var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem);
+ var scopedWriteFs = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fileSystem);
+ var context = new AssembleContext(config, configurationContext, "dev", collector, scopedFs, scopedWriteFs, null, Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"));
A.CallTo(() => mockS3Client.ListObjectsV2Async(A._, A._))
.Returns(new ListObjectsV2Response
{
@@ -178,14 +181,16 @@ bool valid
var mockS3Client = A.Fake();
var fileSystem = new MockFileSystem(new MockFileSystemOptions
{
- CurrentDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly")
+ CurrentDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"),
});
foreach (var i in Enumerable.Range(0, localFiles))
fileSystem.AddFile($"docs/file-{i}.md", new MockFileData($"# Local Document {i}"));
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- var context = new AssembleContext(config, configurationContext, "dev", collector, fileSystem, fileSystem, null, Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"));
+ var scopedFs2 = FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem);
+ var scopedWriteFs2 = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fileSystem);
+ var context = new AssembleContext(config, configurationContext, "dev", collector, scopedFs2, scopedWriteFs2, null, Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"));
var s3Objects = new List();
foreach (var i in Enumerable.Range(0, remoteFiles))
@@ -230,12 +235,14 @@ public async Task TestApply()
{ "docs/update.md", new MockFileData("# Existing Document") },
}, new MockFileSystemOptions
{
- CurrentDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly")
+ CurrentDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly"),
});
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
var checkoutDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "assembly");
- var context = new AssembleContext(config, configurationContext, "dev", collector, fileSystem, fileSystem, null, checkoutDirectory);
+ var scopedFs3 = FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem);
+ var scopedWriteFs3 = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(fileSystem);
+ var context = new AssembleContext(config, configurationContext, "dev", collector, scopedFs3, scopedWriteFs3, null, checkoutDirectory);
var plan = new SyncPlan
{
RemoteListingCompleted = true,
diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs
index 64e26b91af..7f7276a627 100644
--- a/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs
+++ b/tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs
@@ -21,6 +21,7 @@
using Elastic.Documentation.Site.Navigation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Nullean.ScopedFileSystem;
using RazorSlices;
namespace Elastic.Assembler.IntegrationTests;
@@ -46,7 +47,7 @@ public async Task AssertRealNavigation()
var assemblyConfiguration = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
var collector = new TestDiagnosticsCollector(TestContext.Current.TestOutputHelper);
var fs = new FileSystem();
- var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, "dev", collector, fs, new MockFileSystem(), null, null);
+ var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, "dev", collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fs), FileSystemFactory.ScopeCurrentWorkingDirectory(new MockFileSystem()), null, null);
var logFactory = new TestLoggerFactory(TestContext.Current.TestOutputHelper);
var cloner = new AssemblerRepositorySourcer(logFactory, assembleContext);
var checkoutResult = cloner.GetAll();
diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/NavigationRootTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/NavigationRootTests.cs
index 35dcbda1fc..9db9a751e8 100644
--- a/tests-integration/Elastic.Assembler.IntegrationTests/NavigationRootTests.cs
+++ b/tests-integration/Elastic.Assembler.IntegrationTests/NavigationRootTests.cs
@@ -21,6 +21,7 @@
using Elastic.Documentation.Site.Navigation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Nullean.ScopedFileSystem;
using RazorSlices;
namespace Elastic.Assembler.IntegrationTests;
@@ -46,7 +47,7 @@ public async Task AssertRealNavigation()
var assemblyConfiguration = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
var collector = new TestDiagnosticsCollector(TestContext.Current.TestOutputHelper);
var fs = new FileSystem();
- var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, "dev", collector, fs, new MockFileSystem(), null, null);
+ var assembleContext = new AssembleContext(assemblyConfiguration, configurationContext, "dev", collector, FileSystemFactory.ScopeCurrentWorkingDirectory(fs), FileSystemFactory.ScopeCurrentWorkingDirectory(new MockFileSystem()), null, null);
var logFactory = new TestLoggerFactory(TestContext.Current.TestOutputHelper);
var cloner = new AssemblerRepositorySourcer(logFactory, assembleContext);
var checkoutResult = cloner.GetAll();
diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs
index a47e727e68..d89cd503a7 100644
--- a/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs
+++ b/tests-integration/Elastic.Assembler.IntegrationTests/SiteNavigationTests.cs
@@ -15,6 +15,7 @@
using Elastic.Documentation.Navigation.Assembler;
using Elastic.Markdown.IO;
using Microsoft.Extensions.Logging.Abstractions;
+using Nullean.ScopedFileSystem;
namespace Elastic.Assembler.IntegrationTests;
@@ -43,7 +44,8 @@ public SiteNavigationTests(DocumentationFixture fixture, ITestOutputHelper outpu
Collector = new DiagnosticsCollector([]);
var configurationContext = TestHelpers.CreateConfigurationContext(FileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- Context = new AssembleContext(config, configurationContext, "dev", Collector, FileSystem, FileSystem, CheckoutDirectory.FullName, null);
+ var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem);
+ Context = new AssembleContext(config, configurationContext, "dev", Collector, scopedFs, scopedFs, CheckoutDirectory.FullName, null);
}
private Checkout CreateCheckout(IFileSystem fs, Repository repository)
@@ -96,7 +98,8 @@ public async Task ReadAllPathPrefixes()
var fileSystem = new FileSystem();
var configurationContext = TestHelpers.CreateConfigurationContext(fileSystem);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- var context = new AssembleContext(config, configurationContext, "dev", collector, fileSystem, fileSystem, null, null);
+ var scopedFileSystem = FileSystemFactory.ScopeCurrentWorkingDirectory(fileSystem);
+ var context = new AssembleContext(config, configurationContext, "dev", collector, scopedFileSystem, scopedFileSystem, null, null);
var navigationFileInfo = configurationContext.ConfigurationFileProvider.NavigationFile;
var siteNavigationFile = SiteNavigationFile.Deserialize(await FileSystem.File.ReadAllTextAsync(navigationFileInfo.FullName, TestContext.Current.CancellationToken));
@@ -188,7 +191,8 @@ public async Task UriResolving()
var fs = new FileSystem();
var configurationContext = TestHelpers.CreateConfigurationContext(fs);
var config = AssemblyConfiguration.Create(configurationContext.ConfigurationFileProvider);
- var assembleContext = new AssembleContext(config, configurationContext, "prod", collector, fs, fs, null, null);
+ var scopedFs = FileSystemFactory.ScopeCurrentWorkingDirectory(fs);
+ var assembleContext = new AssembleContext(config, configurationContext, "prod", collector, scopedFs, scopedFs, null, null);
var repos = assembleContext.Configuration.AvailableRepositories
.Where(kv => !kv.Value.Skip)
.Select(kv => kv.Value)
@@ -223,7 +227,7 @@ public ValueTask DisposeAsync()
GC.SuppressFinalize(this);
if (TestContext.Current.TestState?.Result is TestResult.Passed)
return default;
- foreach (var resource in _fixture.InMemoryLogger.RecordedLogs)
+ foreach (var resource in _fixture.InMemoryLogger.RecordedLogs.ToList())
_output.WriteLine(resource.Message);
return default;
}
diff --git a/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj b/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj
index f326f6d0cf..047a2e7df2 100644
--- a/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj
+++ b/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj
@@ -9,4 +9,8 @@
+
+
+
+
diff --git a/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs b/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs
index 5decade2f6..7b7946c0a8 100644
--- a/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs
+++ b/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs
@@ -8,6 +8,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
using Microsoft.Extensions.Logging.Abstractions;
+using Nullean.ScopedFileSystem;
namespace Elastic.ApiExplorer.Tests;
@@ -19,7 +20,7 @@ public async Task Reads()
{
var collector = new DiagnosticsCollector([]);
var configurationContext = TestHelpers.CreateConfigurationContext(new FileSystem());
- var context = new BuildContext(collector, new FileSystem(), configurationContext);
+ var context = new BuildContext(collector, FileSystemFactory.RealRead, configurationContext);
context.Configuration.OpenApiSpecifications.Should().NotBeNull().And.NotBeEmpty();
@@ -34,7 +35,7 @@ public async Task Navigation()
{
var collector = new DiagnosticsCollector([]);
var configurationContext = TestHelpers.CreateConfigurationContext(new FileSystem());
- var context = new BuildContext(collector, new FileSystem(), configurationContext);
+ var context = new BuildContext(collector, FileSystemFactory.RealRead, configurationContext);
var generator = new OpenApiGenerator(NullLoggerFactory.Instance, context, NoopMarkdownStringRenderer.Instance);
context.Configuration.OpenApiSpecifications.Should().NotBeNull().And.NotBeEmpty();
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs
index 78eb99c0c2..13f6cbb465 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs
@@ -4,6 +4,7 @@
using AwesomeAssertions;
using Elastic.Changelog.Bundling;
+using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.ReleaseNotes;
namespace Elastic.Changelog.Tests.Changelogs;
@@ -23,7 +24,7 @@ public BundleAmendTests(ITestOutputHelper output) : base(output)
private string CreateChangelogDir()
{
- var changelogDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(changelogDir);
return changelogDir;
}
@@ -48,7 +49,7 @@ private async Task CreateResolvedBundle(CancellationToken ct)
var changelogFile = FileSystem.Path.Join(_changelogDir, "1755268130-existing.yaml");
await FileSystem.File.WriteAllTextAsync(changelogFile, changelog, ct);
- var bundlePath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml");
+ var bundlePath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml");
var input = new BundleChangelogsArguments
{
Directory = _changelogDir,
@@ -87,7 +88,7 @@ private async Task CreateUnresolvedBundle(CancellationToken ct)
var changelogFile = FileSystem.Path.Join(_changelogDir, "1755268130-existing.yaml");
await FileSystem.File.WriteAllTextAsync(changelogFile, changelog, ct);
- var bundlePath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml");
+ var bundlePath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml");
var input = new BundleChangelogsArguments
{
Directory = _changelogDir,
@@ -112,7 +113,7 @@ private async Task CreateUnresolvedBundle(CancellationToken ct)
///
private async Task CreateNewChangelogFile(CancellationToken ct)
{
- var newDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var newDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(newDir);
// language=yaml
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs
index bd2d9eebf9..d508f5271f 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs
@@ -5,6 +5,7 @@
using System.Text;
using AwesomeAssertions;
using Elastic.Changelog.Bundling;
+using Elastic.Documentation.Configuration;
using Elastic.Documentation.Diagnostics;
namespace Elastic.Changelog.Tests.Changelogs;
@@ -24,7 +25,7 @@ public BundleChangelogsTests(ITestOutputHelper output) : base(output)
private string CreateChangelogDir()
{
- var changelogDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(changelogDir);
return changelogDir;
}
@@ -65,7 +66,7 @@ public async Task BundleChangelogs_WithAllOption_CreatesValidBundle()
{
Directory = _changelogDir,
All = true,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -125,7 +126,7 @@ public async Task BundleChangelogs_WithProductsFilter_FiltersCorrectly()
{
Directory = _changelogDir,
InputProducts = [new ProductArgument { Product = "elasticsearch", Target = "9.2.0", Lifecycle = "ga" }],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -192,7 +193,7 @@ public async Task BundleChangelogs_WithPrsFilter_FiltersCorrectly()
{
Directory = _changelogDir,
Prs = ["https://github.com/elastic/elasticsearch/pull/100", "https://github.com/elastic/elasticsearch/pull/200"],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -257,7 +258,7 @@ public async Task BundleChangelogs_WithIssuesFilter_FiltersCorrectly()
{
Directory = _changelogDir,
Issues = ["https://github.com/elastic/elasticsearch/issues/100", "https://github.com/elastic/elasticsearch/issues/200"],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -295,7 +296,7 @@ public async Task BundleChangelogs_WithOldPrFormat_StillMatchesWhenFilteringByPr
{
Directory = _changelogDir,
Prs = ["https://github.com/elastic/elasticsearch/pull/999"],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
var result = await Service.BundleChangelogs(Collector, input, TestContext.Current.CancellationToken);
@@ -336,7 +337,7 @@ public async Task BundleChangelogs_WithPrsFilterAndUnmatchedPrs_EmitsWarnings()
"https://github.com/elastic/elasticsearch/pull/200",
"https://github.com/elastic/elasticsearch/pull/300"
],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -388,7 +389,7 @@ public async Task BundleChangelogs_WithPrsFileFilter_FiltersCorrectly()
await FileSystem.File.WriteAllTextAsync(file2, changelog2, TestContext.Current.CancellationToken);
// Create PRs file
- var prsFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "prs.txt");
+ var prsFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "prs.txt");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(prsFile)!);
// language=yaml
var prsContent =
@@ -402,7 +403,7 @@ public async Task BundleChangelogs_WithPrsFileFilter_FiltersCorrectly()
{
Directory = _changelogDir,
Prs = [prsFile],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -443,7 +444,7 @@ public async Task BundleChangelogs_WithPrNumberAndOwnerRepo_FiltersCorrectly()
Prs = ["100"],
Owner = "elastic",
Repo = "elasticsearch",
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -481,7 +482,7 @@ public async Task BundleChangelogs_WithShortPrFormat_FiltersCorrectly()
{
Directory = _changelogDir,
Prs = ["elastic/elasticsearch#133609"],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -504,7 +505,7 @@ public async Task BundleChangelogs_WithNoMatchingFiles_ReturnsError()
{
Directory = _changelogDir,
InputProducts = [new ProductArgument { Product = "elasticsearch", Target = "9.2.0", Lifecycle = "ga" }],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -520,13 +521,13 @@ public async Task BundleChangelogs_WithNoMatchingFiles_ReturnsError()
public async Task BundleChangelogs_WithInvalidDirectory_ReturnsError()
{
// Arrange
- var invalidDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "nonexistent");
+ var invalidDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "nonexistent");
var input = new BundleChangelogsArguments
{
Directory = invalidDir,
All = true,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -576,7 +577,7 @@ public async Task BundleChangelogs_WithNoFilterOption_ReturnsError()
var input = new BundleChangelogsArguments
{
Directory = _changelogDir,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -598,7 +599,7 @@ public async Task BundleChangelogs_WithMultipleFilterOptions_ReturnsError()
Directory = _changelogDir,
All = true,
InputProducts = [new ProductArgument { Product = "elasticsearch", Target = "9.2.0", Lifecycle = "ga" }],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -651,7 +652,7 @@ public async Task BundleChangelogs_WithMultipleProducts_CreatesValidBundle()
new ProductArgument { Product = "cloud-serverless", Target = "2025-12-02", Lifecycle = "*" },
new ProductArgument { Product = "cloud-serverless", Target = "2025-12-06", Lifecycle = "*" }
],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -708,7 +709,7 @@ public async Task BundleChangelogs_WithWildcardProductFilter_MatchesAllProducts(
{
Directory = _changelogDir,
InputProducts = [new ProductArgument { Product = "*", Target = "9.2.0", Lifecycle = "ga" }],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -762,7 +763,7 @@ public async Task BundleChangelogs_WithWildcardAllParts_EquivalentToAll()
{
Directory = _changelogDir,
InputProducts = [new ProductArgument { Product = "*", Target = "*", Lifecycle = "*" }],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -830,7 +831,7 @@ public async Task BundleChangelogs_WithPrefixWildcardTarget_MatchesCorrectly()
{
Directory = _changelogDir,
InputProducts = [new ProductArgument { Product = "elasticsearch", Target = "9.3.*", Lifecycle = "*" }],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -852,12 +853,12 @@ public async Task BundleChangelogs_WithNonExistentFileAsPrs_ReturnsError()
// Arrange
// Provide a non-existent file path - should return error since there are no other PRs
- var nonexistentFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "nonexistent.txt");
+ var nonexistentFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "nonexistent.txt");
var input = new BundleChangelogsArguments
{
Directory = _changelogDir,
Prs = [nonexistentFile],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -894,7 +895,7 @@ public async Task BundleChangelogs_WithUrlAsPrs_TreatsAsPrIdentifier()
{
Directory = _changelogDir,
Prs = ["https://github.com/elastic/elasticsearch/pull/123"],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -930,12 +931,12 @@ public async Task BundleChangelogs_WithNonExistentFileAndOtherPrs_EmitsWarning()
await FileSystem.File.WriteAllTextAsync(changelogFile, changelog, TestContext.Current.CancellationToken);
// Provide a non-existent file path along with a valid PR - should emit warning for file but continue with PR
- var nonexistentFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "nonexistent.txt");
+ var nonexistentFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "nonexistent.txt");
var input = new BundleChangelogsArguments
{
Directory = _changelogDir,
Prs = [nonexistentFile, "https://github.com/elastic/elasticsearch/pull/123"],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -996,7 +997,7 @@ public async Task BundleChangelogs_WithOutputProducts_OverridesChangelogProducts
new ProductArgument { Product = "cloud-serverless", Target = "2025-12-02", Lifecycle = "ga" },
new ProductArgument { Product = "cloud-serverless", Target = "2025-12-06", Lifecycle = "beta" }
],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1074,7 +1075,7 @@ public async Task BundleChangelogs_WithMultipleProducts_IncludesAllProducts()
{
Directory = _changelogDir,
All = true,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1136,7 +1137,7 @@ public async Task BundleChangelogs_WithInputProducts_IncludesLifecycleInProducts
new ProductArgument { Product = "elasticsearch", Target = "9.2.0", Lifecycle = "ga" },
new ProductArgument { Product = "elasticsearch", Target = "9.3.0", Lifecycle = "beta" }
],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1184,7 +1185,7 @@ public async Task BundleChangelogs_WithOutputProducts_IncludesLifecycleInProduct
new ProductArgument { Product = "cloud-serverless", Target = "2025-12-02", Lifecycle = "ga" },
new ProductArgument { Product = "cloud-serverless", Target = "2025-12-06", Lifecycle = "beta" }
],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1242,7 +1243,7 @@ public async Task BundleChangelogs_ExtractsLifecycleFromChangelogEntries()
{
Directory = _changelogDir,
All = true,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1290,7 +1291,7 @@ public async Task BundleChangelogs_WithInputProductsWildcardLifecycle_ExtractsAc
[
new ProductArgument { Product = "elasticsearch", Target = "9.2.0", Lifecycle = "*" }
],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1364,7 +1365,7 @@ public async Task BundleChangelogs_WithMultipleTargets_WarningIncludesLifecycle(
{
Directory = _changelogDir,
All = true,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1410,7 +1411,7 @@ public async Task BundleChangelogs_WithResolve_CopiesChangelogContents()
Directory = _changelogDir,
All = true,
Resolve = true,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1448,7 +1449,7 @@ public async Task BundleChangelogs_WithExplicitResolveFalse_OverridesConfigResol
resolve: true
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -1473,7 +1474,7 @@ public async Task BundleChangelogs_WithExplicitResolveFalse_OverridesConfigResol
All = true,
Resolve = false,
Config = configPath,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1519,7 +1520,7 @@ public async Task BundleChangelogs_WithResolve_PreservesSpecialCharactersInUtf8(
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-special-chars.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, Encoding.UTF8, TestContext.Current.CancellationToken);
- var outputPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml");
+ var outputPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml");
var input = new BundleChangelogsArguments
{
Directory = _changelogDir,
@@ -1600,7 +1601,7 @@ public async Task BundleChangelogs_WithDirectoryOutputPath_CreatesDefaultFilenam
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
// Use a directory path with default filename (simulating command layer processing)
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
var outputPath = FileSystem.Path.Join(outputDir, "changelog-bundle.yaml");
var input = new BundleChangelogsArguments
@@ -1647,7 +1648,7 @@ public async Task BundleChangelogs_WithResolveAndMissingTitle_ReturnsError()
Directory = _changelogDir,
All = true,
Resolve = true,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1681,7 +1682,7 @@ public async Task BundleChangelogs_WithResolveAndMissingType_ReturnsError()
Directory = _changelogDir,
All = true,
Resolve = true,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1713,7 +1714,7 @@ public async Task BundleChangelogs_WithResolveAndMissingProducts_ReturnsError()
Directory = _changelogDir,
All = true,
Resolve = true,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1747,7 +1748,7 @@ public async Task BundleChangelogs_WithResolveAndInvalidProduct_ReturnsError()
Directory = _changelogDir,
All = true,
Resolve = true,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1785,7 +1786,7 @@ public async Task BundleChangelogs_WithHideFeaturesOption_IncludesHideFeaturesIn
Directory = _changelogDir,
All = true,
HideFeatures = ["feature:hidden-api", "feature:another-hidden"],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1827,7 +1828,7 @@ public async Task BundleChangelogs_WithoutHideFeaturesOption_OmitsHideFeaturesFi
Directory = _changelogDir,
All = true,
// No HideFeatures
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1864,7 +1865,7 @@ public async Task BundleChangelogs_WithHideFeaturesFromFile_IncludesHideFeatures
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
// Create feature IDs file
- var featureIdsFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "feature-ids.txt");
+ var featureIdsFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "feature-ids.txt");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(featureIdsFile)!);
await FileSystem.File.WriteAllTextAsync(featureIdsFile, "feature:from-file\nfeature:another", TestContext.Current.CancellationToken);
@@ -1873,7 +1874,7 @@ public async Task BundleChangelogs_WithHideFeaturesFromFile_IncludesHideFeatures
Directory = _changelogDir,
All = true,
HideFeatures = [featureIdsFile],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1915,7 +1916,7 @@ public async Task BundleChangelogs_WithRepoOption_IncludesRepoInBundleProducts()
Directory = _changelogDir,
All = true,
Repo = "cloud", // Set repo to "cloud" - different from product ID "cloud-serverless"
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1956,7 +1957,7 @@ public async Task BundleChangelogs_WithoutRepoOption_OmitsRepoFieldInOutput()
Directory = _changelogDir,
All = true,
// No --repo option
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -1985,7 +1986,7 @@ public async Task BundleChangelogs_WithBundleLevelRepoConfig_UsesConfigRepoWhenO
owner: elastic
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -2005,7 +2006,7 @@ public async Task BundleChangelogs_WithBundleLevelRepoConfig_UsesConfigRepoWhenO
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-serverless-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml");
+ var outputPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(outputPath)!);
var input = new BundleChangelogsArguments
@@ -2040,7 +2041,7 @@ public async Task BundleChangelogs_WithRepoOptionAndBundleLevelConfig_CliOptionT
repo: wrong-repo
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -2060,7 +2061,7 @@ public async Task BundleChangelogs_WithRepoOptionAndBundleLevelConfig_CliOptionT
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-serverless-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml");
+ var outputPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(outputPath)!);
var input = new BundleChangelogsArguments
@@ -2114,7 +2115,7 @@ public async Task BundleChangelogs_WithOutputProductsAndRepo_IncludesRepoInAllPr
new ProductArgument { Product = "cloud-serverless", Target = "2025-12-02", Lifecycle = "ga" },
new ProductArgument { Product = "elasticsearch-serverless", Target = "2025-12-02", Lifecycle = "ga" }
],
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -2137,7 +2138,7 @@ public async Task BundleChangelogs_WithConfigOutputDirectory_WhenOutputNotSpecif
{
// Arrange - When --output is not specified, use bundle.output_directory from config if set
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
// language=yaml
@@ -2148,7 +2149,7 @@ public async Task BundleChangelogs_WithConfigOutputDirectory_WhenOutputNotSpecif
output_directory: "{outputDir.Replace("\\", "/")}"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), "config-output-dir", "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, "config-output-dir", "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -2195,7 +2196,7 @@ public async Task BundleChangelogs_WithConfigDirectory_WhenDirectoryNotSpecified
{
// Arrange - When --directory is not specified (null), use bundle.directory from config if set
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
// language=yaml
@@ -2206,7 +2207,7 @@ public async Task BundleChangelogs_WithConfigDirectory_WhenDirectoryNotSpecified
output_directory: "{outputDir.Replace("\\", "/")}"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), "config-dir", "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, "config-dir", "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -2254,9 +2255,9 @@ public async Task BundleChangelogs_WithExplicitDirectory_OverridesConfigDirector
// Arrange - config has directory pointing elsewhere, but CLI passes --directory explicitly.
// The explicit CLI value must win (e.g. --directory . when cwd has changelogs).
- var configDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var configDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(configDir);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
// language=yaml
@@ -2267,7 +2268,7 @@ public async Task BundleChangelogs_WithExplicitDirectory_OverridesConfigDirector
output_directory: "{outputDir.Replace("\\", "/")}"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), "config-dir-override", "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, "config-dir-override", "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -2324,7 +2325,7 @@ public async Task BundleChangelogs_WithProfileHideFeatures_IncludesHideFeaturesI
- feature:another-profile-hidden
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), "config", "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, "config", "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -2345,7 +2346,7 @@ public async Task BundleChangelogs_WithProfileHideFeatures_IncludesHideFeaturesI
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
var input = new BundleChangelogsArguments
@@ -2394,7 +2395,7 @@ public async Task BundleChangelogs_WithProfile_OnlyProfileHideFeaturesAreUsed()
- feature:from-profile
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), "config2", "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, "config2", "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -2414,7 +2415,7 @@ public async Task BundleChangelogs_WithProfile_OnlyProfileHideFeaturesAreUsed()
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
var input = new BundleChangelogsArguments
@@ -2461,7 +2462,7 @@ public async Task BundleChangelogs_WithProfileMultipleHideFeatures_AllProfileFea
- feature:profile-two
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), "config3", "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, "config3", "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -2481,7 +2482,7 @@ public async Task BundleChangelogs_WithProfileMultipleHideFeatures_AllProfileFea
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
var input = new BundleChangelogsArguments
@@ -2547,7 +2548,7 @@ public async Task BundleChangelogs_WithComments_ProducesNormalizedChecksum()
{
Directory = _changelogDir,
All = true,
- Output = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml")
+ Output = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml")
};
// Act
@@ -2603,24 +2604,24 @@ public async Task BundleChangelogs_WithAndWithoutComments_ProduceSameChecksum()
""";
// Bundle with comments
- var dir1 = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var dir1 = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(dir1);
var file1 = FileSystem.Path.Join(dir1, "1755268130-shared.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelogWithComments, TestContext.Current.CancellationToken);
- var output1 = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle1.yaml");
+ var output1 = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle1.yaml");
var result1 = await Service.BundleChangelogs(Collector, new BundleChangelogsArguments
{
Directory = dir1, All = true, Output = output1
}, TestContext.Current.CancellationToken);
// Bundle without comments
- var dir2 = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var dir2 = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(dir2);
var file2 = FileSystem.Path.Join(dir2, "1755268130-shared.yaml");
await FileSystem.File.WriteAllTextAsync(file2, changelogWithoutComments, TestContext.Current.CancellationToken);
- var output2 = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle2.yaml");
+ var output2 = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle2.yaml");
var result2 = await Service.BundleChangelogs(Collector, new BundleChangelogsArguments
{
Directory = dir2, All = true, Output = output2
@@ -2671,22 +2672,22 @@ public async Task BundleChangelogs_WithDifferentData_ProducesDifferentChecksum()
""";
// Bundle first file
- var dir1 = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var dir1 = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(dir1);
await FileSystem.File.WriteAllTextAsync(FileSystem.Path.Join(dir1, "1755268130-a.yaml"), changelog1, TestContext.Current.CancellationToken);
- var output1 = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle1.yaml");
+ var output1 = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle1.yaml");
await Service.BundleChangelogs(Collector, new BundleChangelogsArguments
{
Directory = dir1, All = true, Output = output1
}, TestContext.Current.CancellationToken);
// Bundle second file
- var dir2 = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var dir2 = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(dir2);
await FileSystem.File.WriteAllTextAsync(FileSystem.Path.Join(dir2, "1755268130-b.yaml"), changelog2, TestContext.Current.CancellationToken);
- var output2 = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle2.yaml");
+ var output2 = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle2.yaml");
await Service.BundleChangelogs(Collector, new BundleChangelogsArguments
{
Directory = dir2, All = true, Output = output2
@@ -2709,7 +2710,7 @@ public async Task AmendBundle_WithComments_ProducesNormalizedChecksum()
// Arrange - Amend service should also use normalized checksums
// Create a base bundle file
- var bundleDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var bundleDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(bundleDir);
var bundleFile = FileSystem.Path.Join(bundleDir, "bundle.yaml");
@@ -2724,7 +2725,7 @@ public async Task AmendBundle_WithComments_ProducesNormalizedChecksum()
await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken);
// Create a changelog file with comments
- var changelogDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(changelogDir);
// language=yaml
@@ -2777,7 +2778,7 @@ public async Task AmendBundle_WithResolve_ProducesNormalizedChecksum()
// Arrange - Amend with --resolve should also use normalized checksums
// Create a base bundle file
- var bundleDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var bundleDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(bundleDir);
var bundleFile = FileSystem.Path.Join(bundleDir, "bundle.yaml");
@@ -2792,7 +2793,7 @@ public async Task AmendBundle_WithResolve_ProducesNormalizedChecksum()
await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken);
// Create a changelog file with comments
- var changelogDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(changelogDir);
// language=yaml
@@ -2860,7 +2861,7 @@ public async Task BundleChangelogs_WithProfile_OutputProducts_OverridesProductsA
output_products: "elasticsearch {version} ga"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -2880,7 +2881,7 @@ public async Task BundleChangelogs_WithProfile_OutputProducts_OverridesProductsA
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
var input = new BundleChangelogsArguments
@@ -2921,7 +2922,7 @@ public async Task BundleChangelogs_WithProfile_MalformedOutputProducts_EmitsErro
output_products: "elasticsearch {version} ga extra-token"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -2940,7 +2941,7 @@ public async Task BundleChangelogs_WithProfile_MalformedOutputProducts_EmitsErro
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
var input = new BundleChangelogsArguments
@@ -2973,7 +2974,7 @@ public async Task BundleChangelogs_WithProfile_MalformedProductsPattern_EmitsErr
output: "elasticsearch-{version}.yaml"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -2992,7 +2993,7 @@ public async Task BundleChangelogs_WithProfile_MalformedProductsPattern_EmitsErr
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
var input = new BundleChangelogsArguments
@@ -3034,7 +3035,7 @@ public async Task BundleChangelogs_WithProfile_RepoAndOwner_WritesValuesToProduc
owner: elastic
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -3054,7 +3055,7 @@ public async Task BundleChangelogs_WithProfile_RepoAndOwner_WritesValuesToProduc
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-serverless-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
var input = new BundleChangelogsArguments
@@ -3098,7 +3099,7 @@ public async Task BundleChangelogs_WithProfile_BundleLevelRepo_AppliesWhenProfil
output: "elasticsearch-{version}.yaml"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -3118,7 +3119,7 @@ public async Task BundleChangelogs_WithProfile_BundleLevelRepo_AppliesWhenProfil
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
var input = new BundleChangelogsArguments
@@ -3161,7 +3162,7 @@ public async Task BundleChangelogs_WithProfile_ProfileRepoOverridesBundleRepo()
repo: elasticsearch
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -3181,7 +3182,7 @@ public async Task BundleChangelogs_WithProfile_ProfileRepoOverridesBundleRepo()
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
var input = new BundleChangelogsArguments
@@ -3224,7 +3225,7 @@ public async Task BundleChangelogs_WithProfile_NoRepoOwner_PreservesExistingFall
output: "elasticsearch-{version}.yaml"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -3244,7 +3245,7 @@ public async Task BundleChangelogs_WithProfile_NoRepoOwner_PreservesExistingFall
var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
var input = new BundleChangelogsArguments
@@ -3281,7 +3282,7 @@ public async Task BundleChangelogs_WithProfileMode_MissingConfig_ReturnsErrorWit
currentDirectory: "/empty-project"
);
cwdFs.Directory.CreateDirectory("/empty-project");
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, cwdFs);
+ var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystemFactory.ScopeCurrentWorkingDirectory(cwdFs));
var input = new BundleChangelogsArguments
{
@@ -3306,25 +3307,26 @@ public async Task BundleChangelogs_WithProfileMode_MissingConfig_ReturnsErrorWit
public async Task BundleChangelogs_WithProfileMode_ConfigAtCurrentDir_LoadsSuccessfully()
{
// Arrange - changelog.yml is at ./changelog.yml (in the current working directory)
+ var root = Paths.WorkingDirectoryRoot.FullName;
var cwdFs = new System.IO.Abstractions.TestingHelpers.MockFileSystem(
null,
- currentDirectory: "/test-root"
+ currentDirectory: root
);
- cwdFs.Directory.CreateDirectory("/test-root");
- cwdFs.Directory.CreateDirectory("/test-root/changelogs");
- cwdFs.Directory.CreateDirectory("/test-root/output");
+ cwdFs.Directory.CreateDirectory(root);
+ cwdFs.Directory.CreateDirectory(Path.Join(root, "changelogs"));
+ cwdFs.Directory.CreateDirectory(Path.Join(root, "output"));
// language=yaml
var configContent =
- """
+ $$"""
bundle:
- directory: /test-root/changelogs
+ directory: {{Path.Join(root, "changelogs")}}
profiles:
es-release:
products: "elasticsearch {version} {lifecycle}"
output: "elasticsearch-{version}.yaml"
""";
- await cwdFs.File.WriteAllTextAsync("/test-root/changelog.yml", configContent, TestContext.Current.CancellationToken);
+ await cwdFs.File.WriteAllTextAsync(Path.Join(root, "changelog.yml"), configContent, TestContext.Current.CancellationToken);
// language=yaml
var changelogContent =
@@ -3338,16 +3340,16 @@ public async Task BundleChangelogs_WithProfileMode_ConfigAtCurrentDir_LoadsSucce
prs:
- https://github.com/elastic/elasticsearch/pull/100
""";
- await cwdFs.File.WriteAllTextAsync("/test-root/changelogs/1755268130-feature.yaml", changelogContent, TestContext.Current.CancellationToken);
+ await cwdFs.File.WriteAllTextAsync(Path.Join(root, "changelogs/1755268130-feature.yaml"), changelogContent, TestContext.Current.CancellationToken);
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, cwdFs);
+ var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystemFactory.ScopeCurrentWorkingDirectory(cwdFs));
var input = new BundleChangelogsArguments
{
Profile = "es-release",
ProfileArgument = "9.2.0",
- OutputDirectory = "/test-root/output"
- // Config intentionally omitted — should discover /test-root/changelog.yml
+ OutputDirectory = Path.Join(root, "output")
+ // Config intentionally omitted — should discover changelog.yml in CWD
};
// Act
@@ -3356,34 +3358,35 @@ public async Task BundleChangelogs_WithProfileMode_ConfigAtCurrentDir_LoadsSucce
// Assert
result.Should().BeTrue($"Expected bundling to succeed. Errors: {string.Join("; ", Collector.Diagnostics.Where(d => d.Severity == Severity.Error).Select(d => d.Message))}");
Collector.Errors.Should().Be(0);
- cwdFs.Directory.GetFiles("/test-root/output", "*.yaml").Should().NotBeEmpty("Expected output file to be created");
+ cwdFs.Directory.GetFiles(Path.Join(root, "output"), "*.yaml").Should().NotBeEmpty("Expected output file to be created");
}
[Fact]
public async Task BundleChangelogs_WithProfileMode_ConfigAtDocsSubdir_LoadsSuccessfully()
{
// Arrange - changelog.yml is at ./docs/changelog.yml (the second discovery candidate)
+ var root = Paths.WorkingDirectoryRoot.FullName;
var cwdFs = new System.IO.Abstractions.TestingHelpers.MockFileSystem(
null,
- currentDirectory: "/test-root"
+ currentDirectory: root
);
- cwdFs.Directory.CreateDirectory("/test-root");
- cwdFs.Directory.CreateDirectory("/test-root/docs");
- cwdFs.Directory.CreateDirectory("/test-root/changelogs");
- cwdFs.Directory.CreateDirectory("/test-root/output");
+ cwdFs.Directory.CreateDirectory(root);
+ cwdFs.Directory.CreateDirectory(Path.Join(root, "docs"));
+ cwdFs.Directory.CreateDirectory(Path.Join(root, "changelogs"));
+ cwdFs.Directory.CreateDirectory(Path.Join(root, "output"));
// language=yaml
var configContent =
- """
+ $$"""
bundle:
- directory: /test-root/changelogs
+ directory: {{Path.Join(root, "changelogs")}}
profiles:
es-release:
products: "elasticsearch {version} {lifecycle}"
output: "elasticsearch-{version}.yaml"
""";
// Config is in docs/ subdir, not in CWD directly
- await cwdFs.File.WriteAllTextAsync("/test-root/docs/changelog.yml", configContent, TestContext.Current.CancellationToken);
+ await cwdFs.File.WriteAllTextAsync(Path.Join(root, "docs/changelog.yml"), configContent, TestContext.Current.CancellationToken);
// language=yaml
var changelogContent =
@@ -3397,16 +3400,16 @@ public async Task BundleChangelogs_WithProfileMode_ConfigAtDocsSubdir_LoadsSucce
prs:
- https://github.com/elastic/elasticsearch/pull/100
""";
- await cwdFs.File.WriteAllTextAsync("/test-root/changelogs/1755268130-feature.yaml", changelogContent, TestContext.Current.CancellationToken);
+ await cwdFs.File.WriteAllTextAsync(Path.Join(root, "changelogs/1755268130-feature.yaml"), changelogContent, TestContext.Current.CancellationToken);
- var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, cwdFs);
+ var service = new ChangelogBundlingService(LoggerFactory, ConfigurationContext, FileSystemFactory.ScopeCurrentWorkingDirectory(cwdFs));
var input = new BundleChangelogsArguments
{
Profile = "es-release",
ProfileArgument = "9.2.0",
- OutputDirectory = "/test-root/output"
- // Config intentionally omitted — should discover /test-root/docs/changelog.yml
+ OutputDirectory = Path.Join(root, "output")
+ // Config intentionally omitted — should discover docs/changelog.yml in CWD
};
// Act
@@ -3415,7 +3418,7 @@ public async Task BundleChangelogs_WithProfileMode_ConfigAtDocsSubdir_LoadsSucce
// Assert
result.Should().BeTrue($"Expected bundling to succeed. Errors: {string.Join("; ", Collector.Diagnostics.Where(d => d.Severity == Severity.Error).Select(d => d.Message))}");
Collector.Errors.Should().Be(0);
- cwdFs.Directory.GetFiles("/test-root/output", "*.yaml").Should().NotBeEmpty("Expected output file to be created");
+ cwdFs.Directory.GetFiles(Path.Join(root, "output"), "*.yaml").Should().NotBeEmpty("Expected output file to be created");
}
// ─── Phase 3: URL list file and combined version+report ─────────────────────────────
@@ -3431,7 +3434,7 @@ public async Task BundleChangelogs_WithProfile_UrlListFile_PrUrls_FiltersCorrect
release:
output: "bundle.yaml"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -3465,7 +3468,7 @@ public async Task BundleChangelogs_WithProfile_UrlListFile_PrUrls_FiltersCorrect
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
await FileSystem.File.WriteAllTextAsync(file2, changelog2, TestContext.Current.CancellationToken);
- var urlFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "prs.txt");
+ var urlFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "prs.txt");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(urlFile)!);
await FileSystem.File.WriteAllTextAsync(
urlFile,
@@ -3506,7 +3509,7 @@ public async Task BundleChangelogs_WithProfile_UrlListFile_IssueUrls_FiltersCorr
release:
output: "bundle.yaml"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -3540,7 +3543,7 @@ public async Task BundleChangelogs_WithProfile_UrlListFile_IssueUrls_FiltersCorr
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
await FileSystem.File.WriteAllTextAsync(file2, changelog2, TestContext.Current.CancellationToken);
- var urlFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "issues.txt");
+ var urlFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "issues.txt");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(urlFile)!);
await FileSystem.File.WriteAllTextAsync(
urlFile,
@@ -3582,7 +3585,7 @@ public async Task BundleChangelogs_WithProfile_UrlListFile_Numbers_ReturnsError(
release:
output: "bundle.yaml"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -3599,7 +3602,7 @@ await FileSystem.File.WriteAllTextAsync(changelogFile,
- https://github.com/elastic/elasticsearch/pull/100
""", TestContext.Current.CancellationToken);
- var urlFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "prs.txt");
+ var urlFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "prs.txt");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(urlFile)!);
await FileSystem.File.WriteAllTextAsync(urlFile, "100\n200\n", TestContext.Current.CancellationToken);
@@ -3635,7 +3638,7 @@ public async Task BundleChangelogs_WithProfile_UrlListFile_MixedPrsAndIssues_Ret
release:
output: "bundle.yaml"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -3652,7 +3655,7 @@ await FileSystem.File.WriteAllTextAsync(changelogFile,
- https://github.com/elastic/elasticsearch/pull/100
""", TestContext.Current.CancellationToken);
- var urlFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "mixed.txt");
+ var urlFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "mixed.txt");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(urlFile)!);
await FileSystem.File.WriteAllTextAsync(
urlFile,
@@ -3693,7 +3696,7 @@ public async Task BundleChangelogs_WithProfile_CombinedVersionAndReport_Substitu
output_products: "cloud-serverless {version}"
output: "serverless-{version}.yaml"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
@@ -3727,7 +3730,7 @@ public async Task BundleChangelogs_WithProfile_CombinedVersionAndReport_Substitu
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);
await FileSystem.File.WriteAllTextAsync(file2, changelog2, TestContext.Current.CancellationToken);
- var urlFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "prs.txt");
+ var urlFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "prs.txt");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(urlFile)!);
await FileSystem.File.WriteAllTextAsync(
urlFile,
@@ -3735,7 +3738,7 @@ await FileSystem.File.WriteAllTextAsync(
TestContext.Current.CancellationToken
);
- var outputDir = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString());
+ var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);
var input = new BundleChangelogsArguments
@@ -3781,16 +3784,16 @@ public async Task BundleChangelogs_WithProfile_CombinedVersion_ReportArgLooksLik
serverless-release:
output: "serverless-{version}.yaml"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
// A "fake" HTML file to act as the profile arg (simulating user accidentally reversing the order)
- var reportFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "report.html");
+ var reportFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "report.html");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(reportFile)!);
await FileSystem.File.WriteAllTextAsync(reportFile, "", TestContext.Current.CancellationToken);
- var urlFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "prs.txt");
+ var urlFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "prs.txt");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(urlFile)!);
await FileSystem.File.WriteAllTextAsync(urlFile, "https://github.com/elastic/cloud/pull/100\n", TestContext.Current.CancellationToken);
@@ -3828,11 +3831,11 @@ public async Task BundleChangelogs_WithProfile_CombinedVersion_ProfileHasProduct
products: "elasticsearch 9.2.0 ga"
output: "bundle.yaml"
""";
- var configPath = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "changelog.yml");
+ var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);
- var urlFile = FileSystem.Path.Join(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "prs.txt");
+ var urlFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "prs.txt");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(urlFile)!);
await FileSystem.File.WriteAllTextAsync(urlFile, "https://github.com/elastic/elasticsearch/pull/100\n", TestContext.Current.CancellationToken);
@@ -3869,7 +3872,7 @@ public async Task BundleChangelogs_WithReportOption_ParsesPromotionReportAndFilt
PR #200