Skip to content

Conversation

@VeryEarly
Copy link
Collaborator

@VeryEarly VeryEarly commented Dec 4, 2025

Description

instruction: https://github.com/VeryEarly/azure-powershell/blob/yabo/tsp-client/tools/AzDev/src/Typespec/README.md

  • will remove legacy doc before merging
  • will update changelog before merging
  • will update autorest.powershell to not delete tspconfig.yaml

Mandatory Checklist

  • SHOULD update ChangeLog.md file(s) appropriately
    • Update src/{{SERVICE}}/{{SERVICE}}/ChangeLog.md.
      • A snippet outlining the change(s) made in the PR should be written under the ## Upcoming Release header in the past tense.
    • Should not change ChangeLog.md if no new release is required, such as fixing test case only.
  • SHOULD regenerate markdown help files if there is cmdlet API change. Instruction
  • SHOULD have proper test coverage for changes in pull request.
  • SHOULD NOT adjust version of module manually in pull request

Copilot AI review requested due to automatic review settings December 4, 2025 10:10
@azure-client-tools-bot-prd
Copy link

Thanks for your contribution! The pull request validation has started. Please revisit this comment for updated status.

@VeryEarly VeryEarly requested review from dolauli and isra-fel December 4, 2025 10:10
@VeryEarly VeryEarly changed the title [Eng] [Eng] Improve typespec devexp with Update-DevTSPModule Dec 4, 2025
Copilot finished reviewing on behalf of VeryEarly December 4, 2025 10:16
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new cmdlet Update-DevTSPModule to the AzDev tooling that automates TypeSpec-based Azure PowerShell module generation. The cmdlet handles downloading TypeSpec configurations from remote GitHub repositories or local paths, merges them with Azure PowerShell-specific configurations, and generates PowerShell modules using the TypeSpec PowerShell emitter.

Key changes:

  • Adds comprehensive TypeSpec workflow automation cmdlet with support for remote and local TSP configurations
  • Implements YAML serialization support in the YamlHelper service
  • Updates build script to clean artifacts directory before rebuilding

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
tools/AzDev/src/Typespec/UpdateTSPModuleCmdlet.cs New cmdlet implementation handling TSP config resolution, git operations, npm dependency installation, directory copying, and TypeSpec compilation - 759 lines of core logic
tools/AzDev/src/Typespec/README.md Documentation covering cmdlet usage, terminology, prerequisites, and various usage scenarios including local/remote TSP sources
tools/AzDev/src/Services/YamlHelper.cs Implements Serialize method to support YAML output generation required by the cmdlet
tools/AzDev/build.ps1 Adds cleanup of artifacts directory before build to ensure clean builds
tools/AzDev/AzDev/AzDev.psd1 Exports the new Update-DevTSPModule cmdlet for module consumers

- ***Execute this cmdlet anywhere under $RootDirectory without explicitly providing it***: Run this cmdlet under any subdirectory of azure-powershell, `RepoRoot` will be calculated.
- ***Pass by parameter***: `Update-DevTSPModule -RepoRoot $RootDirectory`
- ***AzDev Cmdlet***: `Set-DevContext -RepoRoot $RootDirectory`
- **Emitter**: The tool responsible for generating PowerShell code from the TypeSpec definition. This cmdlet specifically works witapprovedh the `@azure-tools/typespec-powershell` emitter.
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

Typo in the word "with" - contains extra characters "approved".

Suggested change
- **Emitter**: The tool responsible for generating PowerShell code from the TypeSpec definition. This cmdlet specifically works witapprovedh the `@azure-tools/typespec-powershell` emitter.
- **Emitter**: The tool responsible for generating PowerShell code from the TypeSpec definition. This cmdlet specifically works with the `@azure-tools/typespec-powershell` emitter.

Copilot uses AI. Check for mistakes.
Directory.Delete(path, true);
}

private async Task installDependencies(string workingDirectory)
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

Method name installDependencies doesn't follow C# naming conventions - methods should use PascalCase. Should be InstallDependencies.

Suggested change
private async Task installDependencies(string workingDirectory)
private async Task InstallDependencies(string workingDirectory)

Copilot uses AI. Check for mistakes.
Comment on lines +517 to +531
Dictionary<string, object> tspLocationPWDContent = YamlHelper.Deserialize<Dictionary<string, object>>(File.ReadAllText(tspLocationPath));
//if tspconfig emitted previously was from local, only record the absolute directory name
if (File.Exists((string)tspLocationPWDContent["directory"]) && string.IsNullOrEmpty((string)tspLocationPWDContent["repo"]) && string.IsNullOrEmpty((string)tspLocationPWDContent["commit"]))
{
if (remoteInfo != (null, null, null, null))
{
throw new ArgumentException("Emitted by local TSP last time, cannot update by remote info. Please provide remote `-TSPLocation`.");
}
return (string)tspLocationPWDContent["directory"];
}
(string RemoteDirectory, string RemoteCommit, string RemoteRepositoryName, string RemoteForkName) = remoteInfo;
//otherwise it was from remote, construct its url
string repo = !string.IsNullOrEmpty(RemoteForkName) ? $"{RemoteForkName}/azure-rest-api-specs" : (!string.IsNullOrEmpty(RemoteRepositoryName) ? RemoteRepositoryName : (string)tspLocationPWDContent["repo"]);
string commit = !string.IsNullOrEmpty(RemoteCommit) ? RemoteCommit : (string)tspLocationPWDContent["commit"];
string directory = !string.IsNullOrEmpty(RemoteDirectory) ? RemoteDirectory : (string)tspLocationPWDContent["directory"];
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The code doesn't validate that the YAML file contains the expected keys before accessing them with dictionary indexing. If "directory", "repo", or "commit" keys are missing, this will throw a KeyNotFoundException which is less clear than a proper validation error.

Recommendation: Add validation:

Dictionary<string, object> tspLocationPWDContent = YamlHelper.Deserialize<Dictionary<string, object>>(File.ReadAllText(tspLocationPath));

if (!tspLocationPWDContent.ContainsKey("directory") || 
    !tspLocationPWDContent.ContainsKey("repo") || 
    !tspLocationPWDContent.ContainsKey("commit"))
{
    throw new ArgumentException($"Invalid tsp-location.yaml format at {tspLocationPath}. Required keys: directory, repo, commit");
}

Copilot uses AI. Check for mistakes.
}
if (string.IsNullOrEmpty(potentialRoot))
{
throw new ArgumentException("Unable to determine Azure PowerShell repository root. Please execute this cmdlet in Azure-PowerShell repository, Or please provide `-RepoRoot` or set it through `Set-DevContext -RepoRoot`.");
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The error message has inconsistent punctuation and grammar. "Azure-PowerShell repository, Or" should not have a comma before "Or", and "Or" should not be capitalized mid-sentence.

Recommendation:

throw new ArgumentException("Unable to determine Azure PowerShell repository root. Please execute this cmdlet in the Azure PowerShell repository, or provide `-RepoRoot` or set it through `Set-DevContext -RepoRoot`.");
Suggested change
throw new ArgumentException("Unable to determine Azure PowerShell repository root. Please execute this cmdlet in Azure-PowerShell repository, Or please provide `-RepoRoot` or set it through `Set-DevContext -RepoRoot`.");
throw new ArgumentException("Unable to determine Azure PowerShell repository root. Please execute this cmdlet in the Azure PowerShell repository, or provide `-RepoRoot` or set it through `Set-DevContext -RepoRoot`.");

Copilot uses AI. Check for mistakes.
try
{
installDependencies(Path.GetDirectoryName(tempTSPLocation)).Wait();
RunCommand(FindNPMCommandFromPath("tsp.cmd"), $"compile ./ --emit {EmitterPath ?? emitterName} --output-dir {emitterOutputDir}", Path.GetDirectoryName(tempTSPLocation)).Wait();
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The hardcoded .cmd extension makes this code Windows-specific and violates cross-platform compatibility requirements. On Unix-like systems (Linux, macOS), npm and tsp commands don't have the .cmd extension.

Recommendation: Detect the platform and use the appropriate command:

private string FindNPMCommandFromPath(string command)
{
    // Remove .cmd extension for cross-platform compatibility
    string baseCommand = command.Replace(".cmd", "");
    
    if (_npmPath == "" || !File.Exists(_npmPath))
    {
        string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
        string npmPath = pathEnv.Split(Path.PathSeparator)
            .FirstOrDefault(path => path.EndsWith("npm"));
        _npmPath = npmPath;
    }
    
    // On Windows, check for .cmd, .bat, or .exe; on Unix, use the command directly
    string commandPath;
    if (Environment.OSVersion.Platform == PlatformID.Win32NT)
    {
        commandPath = Path.Combine(_npmPath, $"{baseCommand}.cmd");
        if (!File.Exists(commandPath))
            commandPath = Path.Combine(_npmPath, $"{baseCommand}.bat");
        if (!File.Exists(commandPath))
            commandPath = Path.Combine(_npmPath, $"{baseCommand}.exe");
    }
    else
    {
        commandPath = Path.Combine(_npmPath, baseCommand);
    }
    
    if (!File.Exists(commandPath))
    {
        throw new FileNotFoundException($"Command '{baseCommand}' not found in system PATH.");
    }
    return commandPath;
}

Copilot generated this review using guidance from repository custom instructions.
return tempTspLocation;
}

//copy sourcDir and put it under destinationDir
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The comment has a typo: "sourcDir" should be "sourceDir". Also, the comment doesn't accurately describe what the method does - it copies the source directory's contents under a subdirectory (with the source directory's name) within the destination directory, not just "put it under" the destination.

Suggested change
//copy sourcDir and put it under destinationDir
// Copies the contents of sourceDir into a subdirectory (named after sourceDir) within destinationDir,
// excluding any files or directories specified in the 'exclude' array.

Copilot uses AI. Check for mistakes.
Comment on lines +361 to +364
await RunCommand("git", $"clone {cloneRepo} {tempDirPath} --no-checkout --filter=tree:0", outDir);
await RunCommand("git", $"sparse-checkout set {path}", tempDirPath);
await RunCommand("git", $"sparse-checkout add {path}", tempDirPath);
await RunCommand("git", $"checkout {commit}", tempDirPath);
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The git clone URL and path parameters are constructed using string interpolation without validation or sanitization. This could allow command injection if the repo, path, or commit variables contain malicious characters (e.g., shell metacharacters, quotes).

Recommendation: Validate or escape the parameters before using them in shell commands, or use a library that handles command execution safely. At minimum:

  1. Validate repo matches expected format (e.g., "owner/repo-name")
  2. Validate commit is a valid 40-character hex SHA
  3. Validate path doesn't contain dangerous characters like ;, |, &, etc.

Copilot uses AI. Check for mistakes.
string cloneRepo = $"https://github.com/{repo}.git";
await RunCommand("git", $"clone {cloneRepo} {tempDirPath} --no-checkout --filter=tree:0", outDir);
await RunCommand("git", $"sparse-checkout set {path}", tempDirPath);
await RunCommand("git", $"sparse-checkout add {path}", tempDirPath);
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The git operations use separate commands which is inefficient. Line 362 and 363 both call sparse-checkout - line 363's sparse-checkout add is redundant since line 362's sparse-checkout set already sets the path.

Recommendation: Remove line 363 (sparse-checkout add) as it duplicates line 362's functionality:

await RunCommand("git", $"clone {cloneRepo} {tempDirPath} --no-checkout --filter=tree:0", outDir);
await RunCommand("git", $"sparse-checkout set {path}", tempDirPath);
await RunCommand("git", $"checkout {commit}", tempDirPath);
Suggested change
await RunCommand("git", $"sparse-checkout add {path}", tempDirPath);

Copilot uses AI. Check for mistakes.
throw new InvalidOperationException($"Failed to prepare temporary directory [{tempDirPath}]: {ex.Message}", ex);
}
CopyDirectory(tspLocation, tempDirPath, ["tsp-output", "node_modules"]);
string tempTspLocation = Path.Combine(tempDirPath, tspLocation, "tspconfig.yaml");
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The path construction is incorrect. After calling CopyDirectory(tspLocation, tempDirPath, ...) which copies the directory and its contents under tempDirPath, line 386 attempts to combine tempDirPath, the full tspLocation path, and "tspconfig.yaml". This will create an invalid path.

Looking at the CopyDirectory method (line 398), it creates currentDir = Path.Combine(destinationDir, dir.Name), which means the source directory is copied as a subdirectory with its name, not its full path.

Recommendation: Use only the directory name, not the full path:

string dirName = Path.GetFileName(tspLocation);
string tempTspLocation = Path.Combine(tempDirPath, dirName, "tspconfig.yaml");
Suggested change
string tempTspLocation = Path.Combine(tempDirPath, tspLocation, "tspconfig.yaml");
string dirName = Path.GetFileName(tspLocation);
string tempTspLocation = Path.Combine(tempDirPath, dirName, "tspconfig.yaml");

Copilot uses AI. Check for mistakes.
file.CopyTo(tempPath, false);
}
foreach (DirectoryInfo subdir in dirs)
{
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The CopyDirectory method only excludes files based on their names (line 404), but doesn't check directory names against the exclude list. This means directories named "tsp-output" or "node_modules" will still be copied recursively.

Recommendation: Add directory exclusion check:

foreach (DirectoryInfo subdir in dirs)
{
    if (exclude != null && Array.Exists(exclude, e => e.Equals(subdir.Name, StringComparison.OrdinalIgnoreCase)))
    {
        continue;
    }
    CopyDirectory(subdir.FullName, currentDir, exclude);
}
Suggested change
{
{
if (exclude != null && Array.Exists(exclude, e => e.Equals(subdir.Name, StringComparison.OrdinalIgnoreCase)))
{
continue;
}

Copilot uses AI. Check for mistakes.
@isra-fel
Copy link
Member

isra-fel commented Dec 4, 2025

/azp run

@azure-pipelines
Copy link
Contributor

Azure Pipelines successfully started running 3 pipeline(s).

Copilot AI review requested due to automatic review settings December 5, 2025 02:17
Copilot finished reviewing on behalf of VeryEarly December 5, 2025 02:24
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 18 comments.

for (int i = 0; i < segments.Length; i++)
{
string segment = segments[i];
if (segment[0] == '{' && segment[^1] == '}')
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

Potential index range error: Using segment[^1] (index from end) requires C# 8.0+ and .NET Standard 2.1+. If this project targets .NET Standard 2.0 or lower, this will cause a compilation error. Verify the target framework or use:

if (segment[0] == '{' && segment[segment.Length - 1] == '}')
{
    string key = segment.Substring(1, segment.Length - 2);

This is already done correctly on line 466, so line 464 should use the same pattern.

Suggested change
if (segment[0] == '{' && segment[^1] == '}')
if (segment[0] == '{' && segment[segment.Length - 1] == '}')

Copilot uses AI. Check for mistakes.

private string FindNPMCommandFromPath(string command)
{
if (_npmPath == "" || !File.Exists(_npmPath))
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The comparison _npmPath == "" should use string.IsNullOrEmpty(_npmPath) for consistency with the rest of the codebase and to handle null cases properly.

Suggested change
if (_npmPath == "" || !File.Exists(_npmPath))
if (string.IsNullOrEmpty(_npmPath) || !File.Exists(_npmPath))

Copilot uses AI. Check for mistakes.
throw new FileNotFoundException($"package.json not found in {workingDirectory}");
}
string args = File.Exists(Path.Combine(workingDirectory, "package-lock.json")) ? "ci" : "install";
await RunCommand(FindNPMCommandFromPath("npm.cmd"), args, workingDirectory);
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The hardcoded npm.cmd command is Windows-specific and will fail on Unix-like systems. See related comment on line 262 for the same issue with tsp.cmd.

Copilot generated this review using guidance from repository custom instructions.
{
throw new ArgumentException($"No emitter-output-dir configured in {TSPLocation}");
}
// if emitter-outout-dir is not absolute, assume it's relative to RepoRoot
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

Typo in the comment: "emitter-outout-dir" should be "emitter-output-dir" (missing the 'p').

Suggested change
// if emitter-outout-dir is not absolute, assume it's relative to RepoRoot
// if emitter-output-dir is not absolute, assume it's relative to RepoRoot

Copilot uses AI. Check for mistakes.
private const string UriRegex = "^https://(?<urlRoot>github|raw.githubusercontent).com/(?<repo>[^/]*/azure-rest-api-specs(-pr)?)/(tree/|blob/)?(?<commit>[0-9a-f]{40})/(?<path>.*)/tspconfig.yaml$";

private const string emitterName = "@azure-tools/typespec-powershell";

Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

Magic string: The tempDirName constant "TempTypeSpecFiles" should be documented with its purpose. Also consider if this name could conflict with user directories or cause issues on case-sensitive file systems.

Suggested change
/// <summary>
/// Name of the temporary directory used to store intermediate TypeSpec files during module update operations.
/// This directory is created in a controlled location and is intended to be cleaned up after use.
/// The name "TempTypeSpecFiles" was chosen for clarity, but developers should be aware of potential
/// conflicts if a directory with the same name exists in the target location, especially on case-sensitive file systems.
/// Consider updating the logic to use a more unique name if this becomes an issue.
/// </summary>

Copilot uses AI. Check for mistakes.

## Prerequisite
- **node version >= 20**
- **typespec compiler installed?**: `npm install -g @typespec/compiler`
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The question mark makes the documentation unclear. This should be a statement, not a question. Consider: "- typespec compiler installed: npm install -g @typespec/compiler"

Suggested change
- **typespec compiler installed?**: `npm install -g @typespec/compiler`
- **typespec compiler installed**: `npm install -g @typespec/compiler`

Copilot uses AI. Check for mistakes.
this.MyInvocation.BoundParameters.ContainsKey(nameof(RemoteRepositoryName)) ||
this.MyInvocation.BoundParameters.ContainsKey(nameof(RemoteForkName)))
{
throw new ArgumentException("Please do not provide `-RemoteDirectory`, `-RemoteCommit`, `-RemoteRepository` or `-RemoteForkName` along with `-TSPLocation`.");
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

Error message refers to -RemoteRepository but the actual parameter name is -RemoteRepositoryName (line 73). This inconsistency will confuse users trying to follow the error message guidance.

Suggested change
throw new ArgumentException("Please do not provide `-RemoteDirectory`, `-RemoteCommit`, `-RemoteRepository` or `-RemoteForkName` along with `-TSPLocation`.");
throw new ArgumentException("Please do not provide `-RemoteDirectory`, `-RemoteCommit`, `-RemoteRepositoryName` or `-RemoteForkName` along with `-TSPLocation`.");

Copilot uses AI. Check for mistakes.
{
private static readonly HttpClient httpClient = new HttpClient();

private const string UriRegex = "^https://(?<urlRoot>github|raw.githubusercontent).com/(?<repo>[^/]*/azure-rest-api-specs(-pr)?)/(tree/|blob/)?(?<commit>[0-9a-f]{40})/(?<path>.*)/tspconfig.yaml$";
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

Magic string: The URI regex pattern is complex and hardcoded. Consider extracting it as a documented constant with an explanation of what it matches. The pattern expects exactly 40 character commit hashes (SHA-1), which could become outdated if GitHub moves to SHA-256 hashes.

Consider:

// Matches GitHub URLs pointing to tspconfig.yaml files with 40-char commit hashes
// Examples: 
// - https://github.com/Azure/azure-rest-api-specs/blob/abc123.../path/tspconfig.yaml
// - https://raw.githubusercontent.com/Azure/azure-rest-api-specs/abc123.../path/tspconfig.yaml
private const string UriRegex = "^https://(?<urlRoot>github|raw.githubusercontent).com/(?<repo>[^/]*/azure-rest-api-specs(-pr)?)/(tree/|blob/)?(?<commit>[0-9a-f]{40})/(?<path>.*)/tspconfig.yaml$";
Suggested change
private const string UriRegex = "^https://(?<urlRoot>github|raw.githubusercontent).com/(?<repo>[^/]*/azure-rest-api-specs(-pr)?)/(tree/|blob/)?(?<commit>[0-9a-f]{40})/(?<path>.*)/tspconfig.yaml$";
/// <summary>
/// Regex pattern to match GitHub URLs pointing to tspconfig.yaml files with 40-character commit hashes (SHA-1).
///
/// Examples:
/// - https://github.com/Azure/azure-rest-api-specs/blob/abc123.../path/tspconfig.yaml
/// - https://raw.githubusercontent.com/Azure/azure-rest-api-specs/abc123.../path/tspconfig.yaml
///
/// Note: The pattern expects exactly 40-character commit hashes (SHA-1). If GitHub moves to SHA-256 hashes (64 characters),
/// this pattern will need to be updated.
/// </summary>
private const string UriRegex = @"^https://(?<urlRoot>github|raw\.githubusercontent)\.com/(?<repo>[^/]*/azure-rest-api-specs(-pr)?)/(tree/|blob/)?(?<commit>[0-9a-f]{40})/(?<path>.*)/tspconfig\.yaml$";

Copilot uses AI. Check for mistakes.
[Cmdlet("Update", "DevTSPModule")]
public class UpdateTSPModuleCmdlet : DevCmdletBase
{
private static readonly HttpClient httpClient = new HttpClient();
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The static HttpClient instance is good for performance, but it's not being disposed properly. While HttpClient is designed to be long-lived and reused, in a cmdlet scenario where the module may be unloaded, there's no cleanup mechanism. Consider implementing IDisposable or using a different pattern that ensures proper resource cleanup when the module is unloaded.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

@VeryEarly VeryEarly Dec 5, 2025

Choose a reason for hiding this comment

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

@copilot see line "using var response = await httpClient.SendAsync(request, cts.Token);" isn't it properly handle?

$module = 'AzDev'
$artifacts = "$PSScriptRoot/../../artifacts"

rm -r "$artifacts/$module"
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The command rm is an alias that may not be available on all systems. Per the repository's cross-platform compatibility guidelines, use the explicit Remove-Item cmdlet for PowerShell 5.1+ compatibility:

Remove-Item -Path "$artifacts/$module" -Recurse -Force -ErrorAction SilentlyContinue

Copilot generated this review using guidance from repository custom instructions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants