Skip to content
Draft
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
76 changes: 76 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# TfsCmdlets - PowerShell Cmdlets for Azure DevOps and Team Foundation Server

Welcome to the TfsCmdlets repository! This document provides instructions for GitHub Copilot Coding Agent to understand this project and how to work with it effectively.

## Project Overview

TfsCmdlets is a PowerShell module offering cmdlets to work with Azure DevOps (formerly known as VSTS) and Team Foundation Server. The project is primarily written in C# and PowerShell.

## Repository Structure

- `PS/`: Contains PowerShell module files, formats, types, and tests
- `CSharp/`: Contains C# source code for the project
- `Docs/`: Documentation files, including release notes
- `Setup/`: Installation setup files
- `BuildTools/`: Tools used during the build process
- `Assets/`: Project assets like icons and images

## Build Instructions

### Prerequisites

- PowerShell 5.1 or later
- .NET SDK 6.0 or later

### How to Build the Project

To build the project, run the following command:

```powershell
./Build.ps1 -Targets Package -Verbose
```

This command builds the project and creates the package with verbose output.

Other common build targets include:

- `Clean`: Cleans the build output directories
- `Compile`: Compiles the code without packaging
- `Test`: Runs the tests
- `Package`: Creates the package for distribution

### Development Flow

1. Make your changes to the codebase
2. Run the build script to verify everything compiles: `./Build.ps1 -Targets Compile -Verbose`
3. Run tests to ensure functionality: `./Build.ps1 -Targets Test -Verbose`
4. Create the package: `./Build.ps1 -Targets Package -Verbose`

## Code Standards

- Follow C# best practices and PowerShell best practices
- Maintain consistent formatting and naming conventions
- Write meaningful XML documentation for public APIs
- Update release notes in the Docs/ReleaseNotes directory when appropriate

## Testing

Tests are located in:
- PowerShell tests: `PS/_Tests/`
- C# tests: `CSharp/TfsCmdlets.Tests.UnitTests/`

Run tests using: `./Build.ps1 -Targets Test -Verbose`

## Documentation

Update documentation when adding new features or making changes:
- For new cmdlets, ensure proper XML documentation is added
- Update release notes for significant changes in `Docs/ReleaseNotes/`

## Common Issues and Solutions

- If the build fails with missing NuGet packages, ensure NuGet package restore is working properly
- If PowerShell execution is restricted, you may need to use `Set-ExecutionPolicy` to allow the build script to run
- Make sure all prerequisites are installed before attempting to build

When working on this project, please ensure your changes align with the existing architecture and follow the established patterns.
32 changes: 32 additions & 0 deletions .github/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Copilot Setup
runs:
using: composite
steps:
# Install PowerShell modules needed for build
- name: Install PowerShell modules
shell: pwsh
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name psake -Scope CurrentUser -Force
Install-Module -Name GitVersion.CommandLine -Scope CurrentUser -Force
Install-Module -Name BuildHelpers -Scope CurrentUser -Force
Install-Module -Name Pester -MinimumVersion 5.0.0 -Scope CurrentUser -Force

# Install .NET SDK
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'

# Restore NuGet packages
- name: Restore NuGet packages
shell: pwsh
run: |
dotnet restore "CSharp/TfsCmdlets.sln"

# Pre-build the solution to validate
- name: Test build environment
shell: pwsh
run: |
Write-Host "Testing build environment..."
./Build.ps1 -Targets Clean -Verbose
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,10 @@
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.color": "Indigo",
"dotnet.preferCSharpExtension": false
"dotnet.preferCSharpExtension": false,
"github.copilot.chat.commitMessageGeneration.instructions": [
{
"text": "Generate messages in en-US, unless instructed otherwise. Add a first line of comments that summarizes the changes in 50 characters or less. If necessary, add a body that provides more details about the changes. Finish with any relevant issue references. Format the body as a bulleted list, grouping by file when there are multiple files in the same commit. If a commit message has been already drafted by the commiter in the commit message input, use it as a starting point for the generated one.",
}
]
}
Empty file modified Build.ps1
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2"><PrivateAssets>all</PrivateAssets><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets></PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.2.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.2.0" />
<!--<PackageReference Include="Microsoft.TeamFoundationServer.ExtendedClient" Version="16.206.0-preview" />-->
Expand Down
116 changes: 116 additions & 0 deletions CSharp/TfsCmdlets/Extensions/ProcessExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace TfsCmdlets.Extensions
{
public static class ProcessExtensions
{
private const uint TH32CS_SNAPPROCESS = 0x00000002;

[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool Process32First(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool Process32Next(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);

[StructLayout(LayoutKind.Sequential)]
private struct PROCESSENTRY32
{
public uint dwSize;
public uint cntUsage;
public uint th32ProcessID;
public IntPtr th32DefaultHeapID;
public uint th32ModuleID;
public uint cntThreads;
public uint th32ParentProcessID;
public int pcPriClassBase;
public uint dwFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szExeFile;
}

/// <summary>
/// Retorna o PID do processo pai, ou 0 se não encontrado.
/// </summary>
public static int GetParentProcessId(this Process process)
{
if (process == null)
throw new ArgumentNullException(nameof(process));

var snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == IntPtr.Zero)
return 0;

try
{
var entry = new PROCESSENTRY32();
entry.dwSize = (uint)Marshal.SizeOf(entry);

if (Process32First(snapshot, ref entry))
{
do
{
if (entry.th32ProcessID == (uint)process.Id)
return (int)entry.th32ParentProcessID;
}
while (Process32Next(snapshot, ref entry));
}
}
finally
{
CloseHandle(snapshot);
}

return 0;
}

/// <summary>
/// Retorna o processo pai, ou null se não encontrado.
/// </summary>
public static Process ParentProcess(this Process process)
{
if (process == null) throw new ArgumentNullException(nameof(process));

int parentPid = process.GetParentProcessId();

if (parentPid == 0) return null;

try
{
return Process.GetProcessById(parentPid);
}
catch
{
return null;
}
}

/// <summary>
/// Retorna o handle da janela principal do processo ou de seu ancestral.
/// </summary>
public static IntPtr WindowHandleRecursive(this Process process)
{
if (process == null)
throw new ArgumentNullException(nameof(process));

var current = process;

while (current is { Id: not 0 })
{
var hwnd = current.MainWindowHandle;
if (hwnd != IntPtr.Zero) return hwnd;

current = current.ParentProcess();
if (current == null) break;
}

return IntPtr.Zero;
}
}
}
2 changes: 2 additions & 0 deletions CSharp/TfsCmdlets/Services/IPowerShellService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public interface IPowerShellService

string WindowTitle { get; set; }

IntPtr WindowHandle { get; }

PSModuleInfo Module { get; }

string CurrentCommand {get;}
Expand Down
43 changes: 37 additions & 6 deletions CSharp/TfsCmdlets/Services/Impl/InteractiveAuthenticationImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System;
using System.Linq;
using System.Composition;
using TfsCmdlets.Services;

namespace TfsCmdlets.Services.Impl
{
Expand All @@ -11,6 +15,15 @@ public class InteractiveAuthenticationImpl : IInteractiveAuthentication
{
private const string CLIENT_ID = "9f44d9a2-86ef-4794-b2b2-f9038a2628e0";
private const string SCOPE_ID = "499b84ac-1321-427f-aa17-267ca6975798/user_impersonation";
private const string CLIENT_NAME = "TfsCmdlets.InteractiveAuth";

private IPowerShellService PowerShell { get; }

[ImportingConstructor]
public InteractiveAuthenticationImpl(IPowerShellService powerShell)
{
PowerShell = powerShell;
}

public string GetToken(Uri uri)
{
Expand All @@ -25,17 +38,17 @@ public string GetToken(Uri uri)
/// </summary>
/// <param name="scopes"></param>
/// <returns>AuthenticationResult</returns>
private static async Task<AuthenticationResult> SignInUserAndGetTokenUsingMSAL(string[] scopes)
private async Task<AuthenticationResult> SignInUserAndGetTokenUsingMSAL(string[] scopes)
{
var application = PublicClientApplicationBuilder
.CreateWithApplicationOptions(new PublicClientApplicationOptions
{
AadAuthorityAudience = AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount,
ClientId = CLIENT_ID,
ClientName = "TfsCmdlets.InteractiveAuth",
// ClientId = CLIENT_ID,
// ClientName = CLIENT_NAME,
ClientVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString()
})
.WithDesktopFeatures()
.WithWindowsDesktopFeatures(new BrokerOptions(enabledOn: BrokerOptions.OperatingSystems.None))
.WithDefaultRedirectUri()
.Build();

Expand All @@ -52,11 +65,29 @@ private static async Task<AuthenticationResult> SignInUserAndGetTokenUsingMSAL(s
var cts = new CancellationTokenSource();
cts.CancelAfter(60000);

return await application
var tokenBuilder = application
.AcquireTokenInteractive(scopes)
.WithPrompt(Prompt.SelectAccount)
.WithParentActivityOrWindow(PowerShell.WindowHandle)
.WithClaims(ex.Claims)
.ExecuteAsync(cts.Token);
.WithUseEmbeddedWebView(false)
.WithSystemWebViewOptions(new SystemWebViewOptions
{
OpenBrowserAsync = (url) =>
{
var encodedUrl = url.AbsoluteUri.Replace(" ", "%20");
var msg = $"Opening browser for authentication. If your browser does not open automatically, please navigate to the following URL:\n\n{encodedUrl}";
PowerShell.CurrentCmdlet.Host.UI.WriteLine(msg);
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = encodedUrl,
UseShellExecute = true
});
return Task.CompletedTask;
}
});

return await tokenBuilder.ExecuteAsync(cts.Token);
}
}
}
Expand Down
15 changes: 14 additions & 1 deletion CSharp/TfsCmdlets/Services/Impl/PowerShellServiceImpl.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.TeamFoundation.Core.WebApi;
using TfsCmdlets.Cmdlets;
using TfsCmdlets.Models;
using OsProcess = System.Diagnostics.Process;

namespace TfsCmdlets.Services.Impl
{
Expand Down Expand Up @@ -201,6 +202,18 @@ public bool IsVerbose

public string Edition => RuntimeUtil.Platform;


public IntPtr WindowHandle {
get
{
if(!IsInteractive || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return IntPtr.Zero;

var process = OsProcess.GetCurrentProcess();

return process.WindowHandleRecursive();
}
}

[ImportingConstructor]
public PowerShellServiceImpl(IRuntimeUtil runtimeUtil)
{
Expand Down
3 changes: 3 additions & 0 deletions CSharp/TfsCmdlets/TfsCmdlets.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="16.*-*" />
<PackageReference Include="Microsoft.VisualStudio.Services.InteractiveClient" Version="16.*-*" />
<PackageReference Include="Microsoft.VisualStudio.Services.Release.Client" Version="16.*-*" />
Expand Down
2 changes: 1 addition & 1 deletion Docs/CommonHelpText.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"HELP_PARAM_CREDENTIAL" = "Specifies a user account that has permission to perform this action. To provide a user name and password, a Personal Access Token, and/or to open a input dialog to enter your credentials, call Get-TfsCredential with the appropriate arguments and pass its return to this argument."
"HELP_PARAM_CACHED_CREDENTIAL" = "Specifies that cached (default) credentials should be used when possible/available."
"HELP_PARAM_PSCREDENTIAL" = "Specifies a user account that has permission to perform this action. The default is the credential of the user under which the PowerShell process is being run - in most cases that corresponds to the user currently logged in. Type a user name, such as 'User01' or 'Domain01User01', or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password."
"HELP_PARAM_INTERACTIVE" = "Prompts for user credentials. Can be used for any Team Foundation Server or Azure DevOps account - the proper login dialog is automatically selected. Should only be used in an interactive PowerShell session (i.e., a PowerShell terminal window), never in an unattended script (such as those executed during an automated build). Currently it is only supported in Windows PowerShell."
"HELP_PARAM_INTERACTIVE" = "Prompts for user credentials. Can be used for any Team Foundation Server or Azure DevOps account - the proper login dialog is automatically selected. Should only be used in an interactive PowerShell session (i.e., a PowerShell terminal window), never in an unattended script (such as those executed during an automated build). Supported in both Windows PowerShell and PowerShell Core (7+)."
"HELP_PARAM_GIT_REPOSITORY" = "Specifies the target Git repository. Valid values are the name of the repository, its ID (a GUID), or a Microsoft.TeamFoundation.SourceControl.WebApi.GitRepository object obtained by e.g. a call to Get-TfsGitRepository. When omitted, defaults to the team project name (i.e. the default repository)."
"HELP_PARAM_NEWNAME" = "Specifies the new name of the item. Enter only a name - i.e., for items that support paths, do not enter a path and name."
"HELP_PARAM_WORKITEM" = "Specifies a work item. Valid values are the work item ID or an instance of Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.WorkItem."
Expand Down
Empty file added _config.yml
Empty file.
Loading