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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@
using TemplatePipeline.Modules;
using TemplatePipeline.Settings;

var pipelineDirectory = PipelineProjectDirectory.Find();
Environment.CurrentDirectory = pipelineDirectory;

var builder = Pipeline.CreateBuilder(args);
var builder = Pipeline.CreateBuilderFromSource(args);

builder.Configuration
.AddJsonFile(Path.Combine(pipelineDirectory, "appsettings.json"), optional: false)
.AddJsonFile("appsettings.json", optional: false)
.AddEnvironmentVariables();

builder.Services.Configure<BuildSettings>(builder.Configuration.GetSection("Build"));
Expand Down
5 changes: 4 additions & 1 deletion src/ModularPipelines/Context/Checksum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

namespace ModularPipelines.Context;

internal class Checksum(IFileSystemProvider fileSystemProvider) : IChecksumContext
internal class Checksum(
IFileSystemProvider fileSystemProvider,
PipelineWorkingDirectory workingDirectory) : IChecksumContext
{
public string Md5(string filePath)
{
filePath = workingDirectory.ResolvePath(filePath);
if (!fileSystemProvider.FileExists(filePath))
{
throw new FileNotFoundException($"Cannot calculate MD5 checksum: file not found at '{filePath}'", filePath);
Expand Down
12 changes: 10 additions & 2 deletions src/ModularPipelines/Context/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal sealed class Command : ICommandContext
private readonly ISecretObfuscator _secretObfuscator;
private readonly ICommandExecutionCounter _commandExecutionCounter;
private readonly IOptions<PipelineOptions> _pipelineOptions;
private readonly PipelineWorkingDirectory _pipelineWorkingDirectory;

public Command(
ICommandLogger commandLogger,
Expand All @@ -46,7 +47,8 @@ public Command(
ISecretRegistry secretRegistry,
ISecretObfuscator secretObfuscator,
ICommandExecutionCounter commandExecutionCounter,
IOptions<PipelineOptions> pipelineOptions)
IOptions<PipelineOptions> pipelineOptions,
PipelineWorkingDirectory pipelineWorkingDirectory)
{
_commandLogger = commandLogger;
_commandLineBuilder = commandLineBuilder;
Expand All @@ -56,6 +58,7 @@ public Command(
_secretObfuscator = secretObfuscator;
_commandExecutionCounter = commandExecutionCounter;
_pipelineOptions = pipelineOptions;
_pipelineWorkingDirectory = pipelineWorkingDirectory;
}

public async Task<CommandResult> ExecuteCommandLineToolAsync(
Expand All @@ -64,7 +67,12 @@ public async Task<CommandResult> ExecuteCommandLineToolAsync(
CancellationToken cancellationToken = default)
{
_commandExecutionCounter.Record(AmbientModuleContext.CurrentModuleType);
var execOpts = executionOptions ?? new CommandExecutionOptions();
var execOpts = (executionOptions ?? new CommandExecutionOptions()) with
{
WorkingDirectory = executionOptions?.WorkingDirectory is { } workingDirectory
? _pipelineWorkingDirectory.ResolvePath(workingDirectory)
: _pipelineWorkingDirectory.Path,
};
RegisterSecrets(options, execOpts);
var (command, commandInput, tool, parsedArgs) = CreateCommand(options, execOpts);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ public interface IEnvironmentDomainContext
string UserName { get; }

/// <summary>
/// Gets the working directory captured when the pipeline context was created.
/// Gets the pipeline's configured working directory.
/// </summary>
/// <remarks>
/// To run a command in another directory, set
/// Set <see cref="PipelineBuilderOptions.WorkingDirectory"/> when creating the pipeline,
/// or override an individual command with
/// <see cref="Options.CommandExecutionOptions.WorkingDirectory"/>.
/// </remarks>
string WorkingDirectory { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ internal class EnvironmentDomainContext : IEnvironmentDomainContext
/// </summary>
/// <param name="variables">The environment variables context.</param>
/// <param name="buildSystem">The build system context.</param>
/// <param name="workingDirectory">The configured pipeline working directory.</param>
public EnvironmentDomainContext(
IEnvironmentVariablesContext variables,
IBuildSystemContext buildSystem)
IBuildSystemContext buildSystem,
PipelineWorkingDirectory workingDirectory)
{
Variables = variables;
BuildSystem = buildSystem;
WorkingDirectory = System.Environment.CurrentDirectory;
WorkingDirectory = workingDirectory.Path;
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ namespace ModularPipelines.Context.Domains.Implementations;
internal class FilesContext(
IFileSystemContext fileSystemContext,
IFileSystemProvider fileSystemProvider,
PipelineWorkingDirectory workingDirectory,
IZipContext zip,
IChecksumContext checksum) : IFilesContext
Comment on lines +14 to 16

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scope nested file helpers to the pipeline directory

When PipelineBuilderOptions.WorkingDirectory is set and the process CWD is somewhere else, only the top-level IFilesContext methods use _workingDirectory; the injected Zip and Checksum helpers still receive raw relative paths and resolve them against the process CWD. A module that writes relative.txt with context.Files.WriteAsync(...) and then calls context.Files.Checksum.Md5("relative.txt"), or zips to "out.zip", will fail or create artifacts outside the configured pipeline directory, so these nested helpers need the same path resolver or scoped wrappers.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 48b417c. Zip and Checksum now receive PipelineWorkingDirectory and resolve all relative ZIP input/output and checksum paths against it. The integration test writes, hashes, zips, and unzips using relative paths and asserts the configured directory.

{
private readonly IFileSystemContext _fileSystemContext = fileSystemContext;
private readonly IFileSystemProvider _fileSystemProvider = fileSystemProvider;
private readonly PipelineWorkingDirectory _workingDirectory = workingDirectory;

/// <inheritdoc />
public File GetFile(string path) => _fileSystemContext.GetFile(path);
Expand All @@ -28,12 +30,12 @@ internal class FilesContext(

/// <inheritdoc />
public IEnumerable<File> Glob(string pattern) =>
GetFolder(System.Environment.CurrentDirectory).GetFiles(pattern);
GetFolder(_workingDirectory.Path).GetFiles(pattern);

/// <inheritdoc />
public IEnumerable<Folder> GlobFolders(string pattern)
{
var currentDirectory = System.Environment.CurrentDirectory;
var currentDirectory = _workingDirectory.Path;
var matcher = new Matcher(StringComparison.OrdinalIgnoreCase)
.AddInclude(pattern);

Expand All @@ -47,19 +49,20 @@ public IEnumerable<Folder> GlobFolders(string pattern)

/// <inheritdoc />
public Task<string> ReadAsync(string path, CancellationToken cancellationToken = default)
=> _fileSystemProvider.ReadAllTextAsync(path, cancellationToken);
=> _fileSystemProvider.ReadAllTextAsync(_workingDirectory.ResolvePath(path), cancellationToken);

/// <inheritdoc />
public Task WriteAsync(string path, string content, CancellationToken cancellationToken = default)
=> _fileSystemProvider.WriteAllTextAsync(path, content, cancellationToken);
=> _fileSystemProvider.WriteAllTextAsync(_workingDirectory.ResolvePath(path), content, cancellationToken);

/// <inheritdoc />
public Task<bool> ExistsAsync(string path, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var resolvedPath = _workingDirectory.ResolvePath(path);
return Task.FromResult(
_fileSystemProvider.FileExists(path)
|| _fileSystemProvider.DirectoryExists(path));
_fileSystemProvider.FileExists(resolvedPath)
|| _fileSystemProvider.DirectoryExists(resolvedPath));
}

/// <inheritdoc />
Expand Down
8 changes: 5 additions & 3 deletions src/ModularPipelines/Context/EnvironmentContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ internal class EnvironmentContext : IEnvironmentContext
private readonly IHostEnvironment _hostEnvironment;

public EnvironmentContext(IHostEnvironment hostEnvironment,
IEnvironmentVariablesContext environmentVariables)
IEnvironmentVariablesContext environmentVariables,
PipelineWorkingDirectory workingDirectory)
{
_hostEnvironment = hostEnvironment;
EnvironmentVariables = environmentVariables;
ContentDirectory = _hostEnvironment.ContentRootPath!;
WorkingDirectory = new Folder(workingDirectory.Path);

OperatingSystem = OperatingSystemHelper.GetOperatingSystem();
}
Expand All @@ -30,12 +32,12 @@ public EnvironmentContext(IHostEnvironment hostEnvironment,

public Folder ContentDirectory { get; }

public Folder WorkingDirectory { get; } = Environment.CurrentDirectory!;
public Folder WorkingDirectory { get; }

public IEnvironmentVariablesContext EnvironmentVariables { get; }

public Folder? GetFolder(Environment.SpecialFolder specialFolder)
{
return Environment.GetFolderPath(specialFolder);
}
}
}
20 changes: 13 additions & 7 deletions src/ModularPipelines/Context/FileSystemContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,27 @@ namespace ModularPipelines.Context;
internal class FileSystemContext : IFileSystemContext
{
private readonly IFileSystemProvider _provider;
private readonly PipelineWorkingDirectory _workingDirectory;

public FileSystemContext(IFileSystemProvider provider)
public FileSystemContext(
IFileSystemProvider provider,
PipelineWorkingDirectory workingDirectory)
{
_provider = provider;
_workingDirectory = workingDirectory;
}

public void DeleteFile(File file) => file.Delete();

public void DeleteFolder(Folder folder) => folder.Delete();

public File CopyFile(File file, string destinationFilePath) => file.CopyTo(destinationFilePath);
public File CopyFile(File file, string destinationFilePath) => file.CopyTo(ResolvePath(destinationFilePath));

public Folder CopyFolder(Folder folder, string destinationFolder) => folder.CopyTo(destinationFolder);
public Folder CopyFolder(Folder folder, string destinationFolder) => folder.CopyTo(ResolvePath(destinationFolder));

public void MoveFile(File file, string destinationFilePath) => file.MoveTo(destinationFilePath);
public void MoveFile(File file, string destinationFilePath) => file.MoveTo(ResolvePath(destinationFilePath));

public void MoveFolder(Folder folder, string destinationFolderPath) => folder.MoveTo(destinationFolderPath);
public void MoveFolder(Folder folder, string destinationFolderPath) => folder.MoveTo(ResolvePath(destinationFolderPath));

public bool FileExists(File file) => file.Exists;

Expand All @@ -36,7 +40,7 @@ public FileSystemContext(IFileSystemProvider provider)

public void SetFolderAttributes(Folder folder, FileAttributes attributes) => folder.Attributes = attributes;

public File GetFile(string filePath) => new(filePath, _provider);
public File GetFile(string filePath) => new(ResolvePath(filePath), _provider);

public IEnumerable<File> GetFiles(Folder rootFolder, Func<File, bool> predicate)
{
Expand All @@ -48,7 +52,7 @@ public IEnumerable<Folder> GetFolders(Folder rootFolder, Func<Folder, bool> pred
return rootFolder.GetFolders(predicate);
}

public Folder GetFolder(string path) => new(path, _provider);
public Folder GetFolder(string path) => new(ResolvePath(path), _provider);

public Folder GetFolder(Environment.SpecialFolder specialFolder)
{
Expand All @@ -66,4 +70,6 @@ public string GetNewTemporaryFilePath()
{
return _provider.Combine(_provider.GetTempPath(), _provider.GetRandomFileName());
}

private string ResolvePath(string path) => _workingDirectory.ResolvePath(path);
}
14 changes: 7 additions & 7 deletions src/ModularPipelines/Context/IEnvironmentContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ public interface IEnvironmentContext
/// <inheritdoc cref="IHostEnvironment.ContentRootPath"/>
/// <remarks>
/// This property is immutable after pipeline initialization.
/// If you need to change the working directory for command execution,
/// use command options or <see cref="System.Environment.CurrentDirectory"/> directly.
/// </remarks>
public Folder ContentDirectory { get; }

/// <inheritdoc cref="Environment.CurrentDirectory"/>
/// <summary>
/// Gets the pipeline's configured working directory.
/// </summary>
/// <remarks>
/// This property captures the working directory at pipeline initialization time and is immutable.
/// If you need to change the working directory for command execution,
/// use command options or <see cref="System.Environment.CurrentDirectory"/> directly.
/// This property is immutable after pipeline initialization. Set
/// <see cref="PipelineBuilderOptions.WorkingDirectory"/> when creating the pipeline,
/// or override an individual command with <see cref="Options.CommandExecutionOptions.WorkingDirectory"/>.
/// </remarks>
public Folder WorkingDirectory { get; }

Expand All @@ -56,4 +56,4 @@ public interface IEnvironmentContext
/// Gets the Environment Variables available to this Pipeline.
/// </summary>
public IEnvironmentVariablesContext EnvironmentVariables { get; }
}
}
8 changes: 7 additions & 1 deletion src/ModularPipelines/Context/Zip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@

namespace ModularPipelines.Context;

internal class Zip(IFileSystemProvider fileSystemProvider) : IZipContext
internal class Zip(
IFileSystemProvider fileSystemProvider,
PipelineWorkingDirectory workingDirectory) : IZipContext
{
private readonly IFileSystemProvider _fileSystemProvider = fileSystemProvider;
private readonly PipelineWorkingDirectory _workingDirectory = workingDirectory;

public File ZipFolder(Folder folder, string outputPath, CompressionLevel compressionLevel)
{
outputPath = _workingDirectory.ResolvePath(outputPath);
var outputIsDirectory = _fileSystemProvider.DirectoryExists(outputPath)
|| (!_fileSystemProvider.FileExists(outputPath)
&& IsDirectoryPath(outputPath));
Expand Down Expand Up @@ -74,6 +78,8 @@ public Folder UnZipToFolder(string zipPath, string outputFolderPath, bool overwr
{
ArgumentException.ThrowIfNullOrWhiteSpace(zipPath);
ArgumentException.ThrowIfNullOrWhiteSpace(outputFolderPath);
zipPath = _workingDirectory.ResolvePath(zipPath);
outputFolderPath = _workingDirectory.ResolvePath(outputFolderPath);

if (!_fileSystemProvider.FileExists(zipPath))
{
Expand Down
28 changes: 27 additions & 1 deletion src/ModularPipelines/Pipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,40 @@ public static class Pipeline
/// </example>
public static PipelineBuilder CreateBuilder(string[]? args = null)
{
return new PipelineBuilder(args);
return new PipelineBuilder(new PipelineBuilderOptions { Args = args });
}

/// <summary>
/// Creates a new pipeline builder whose working directory is inferred from the calling source file.
/// </summary>
/// <param name="args">Optional command line arguments.</param>
/// <param name="sourceFilePath">The calling source file path, supplied by the compiler.</param>
/// <returns>A new pipeline builder instance.</returns>
/// <remarks>
/// <c>MODULAR_PIPELINES_DIRECTORY</c> can override the inferred project directory. If neither can
/// be resolved, the process working directory is used.
/// </remarks>
public static PipelineBuilder CreateBuilderFromSource(
string[]? args = null,
[System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "")
{
return new PipelineBuilder(new PipelineBuilderOptions
{
Args = args,
WorkingDirectory = PipelineDirectory.TryFindPipelineProject(sourceFilePath),
});
}

/// <summary>
/// Creates a new pipeline builder with the specified options.
/// </summary>
/// <param name="options">The builder options.</param>
/// <returns>A new pipeline builder instance.</returns>
/// <remarks>
/// This overload does not infer a pipeline project directory. When
/// <see cref="PipelineBuilderOptions.WorkingDirectory"/> is
/// unset, the configured content root is used, falling back to the process working directory.
/// </remarks>
/// <example>
/// <code>
/// var builder = Pipeline.CreateBuilder(new PipelineBuilderOptions
Expand Down
Loading
Loading