Skip to content

Commit 645b1fd

Browse files
authored
Add new User cmdlets (#224)
* Update obsolete API * Remove automated tweet * Add new New-TfsUser cmdlet * Update build tasks to use PowerShell 7 * Remove docgen from build * Build before debug * Add alias * Update manifest version * User three-part version for WinGet package * Revert change * Move JSON-related extensions to separate file * Rename property to avoid conflict with DisplayName arguments * Move template-based API call support to IRestApiService * Add automatic conversion of JSON objects to PowerShell format * Add new cmdlet * Add Remove-TfsUser * Fix typo * Update release notes +semver: minor * Add proper support for project/team parameters
1 parent 4e35fb0 commit 645b1fd

File tree

20 files changed

+594
-159
lines changed

20 files changed

+594
-159
lines changed

.github/workflows/main.yml

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -322,16 +322,19 @@ jobs:
322322
needs: [ Release, Nuget, PSGallery, Chocolatey, Site, WinGet ]
323323
environment: announcements
324324
steps:
325-
- name: Tweet
326-
id: tweet
327-
uses: snow-actions/[email protected]
328-
env:
329-
BUILD_NAME: ${{ needs.Release.outputs.BUILD_NAME }}
330-
CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
331-
CONSUMER_API_SECRET_KEY: ${{ secrets.TWITTER_CONSUMER_API_SECRET_KEY }}
332-
ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
333-
ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
334-
RELEASE_TAG: ${{ needs.Release.outputs.RELEASE_TAG }}
335-
with:
336-
status: |
337-
TfsCmdlets version ${{ env.BUILD_NAME }} has just been released. Check it out! https://github.com/igoravl/TfsCmdlets/releases/tag/v${{ env.RELEASE_TAG }}
325+
- name: No-op
326+
shell: bash
327+
run: echo "no-op"
328+
# - name: Tweet
329+
# id: tweet
330+
# uses: snow-actions/[email protected]
331+
# env:
332+
# BUILD_NAME: ${{ needs.Release.outputs.BUILD_NAME }}
333+
# CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
334+
# CONSUMER_API_SECRET_KEY: ${{ secrets.TWITTER_CONSUMER_API_SECRET_KEY }}
335+
# ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
336+
# ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
337+
# RELEASE_TAG: ${{ needs.Release.outputs.RELEASE_TAG }}
338+
# with:
339+
# status: |
340+
# TfsCmdlets version ${{ env.BUILD_NAME }} has just been released. Check it out! https://github.com/igoravl/TfsCmdlets/releases/tag/v${{ env.RELEASE_TAG }}

.vscode/launch.json

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,6 @@
2222
"console": "externalTerminal",
2323
"preLaunchTask": "Build"
2424
},
25-
{
26-
"name": "(.NET) PowerShell 7 Preview",
27-
"type": "coreclr",
28-
"request": "launch",
29-
"program": "C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe",
30-
"args": [
31-
"-noexit",
32-
"-command",
33-
"Import-Module ${workspaceFolder}/out/module/TfsCmdlets.psd1; Enter-TfsShell"
34-
],
35-
"cwd": "${workspaceFolder}",
36-
"console": "externalTerminal",
37-
"preLaunchTask": "Build"
38-
},
3925
{
4026
"name": "(.NET) PowerShell 7",
4127
"type": "coreclr",
@@ -47,7 +33,8 @@
4733
"Import-Module ${workspaceFolder}/out/module/TfsCmdlets.psd1; Enter-TfsShell"
4834
],
4935
"cwd": "${workspaceFolder}",
50-
"console": "externalTerminal"
36+
"console": "externalTerminal",
37+
"preLaunchTask": "Build"
5138
}
5239
]
5340
}

.vscode/tasks.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"tasks": [
55
{
66
"label": "Build",
7-
"command": "c:\\windows\\system32\\WindowsPowerShell\\v1.0\\powershell.exe",
7+
"command": "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
88
"group": {
99
"kind": "build",
1010
"isDefault": true
@@ -35,7 +35,7 @@
3535
},
3636
{
3737
"label": "Rebuild",
38-
"command": "c:\\windows\\system32\\WindowsPowerShell\\v1.0\\powershell.exe",
38+
"command": "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
3939
"presentation": {
4040
"echo": true,
4141
"reveal": "always",
@@ -62,7 +62,7 @@
6262
},
6363
{
6464
"label": "Clean",
65-
"command": "c:\\windows\\system32\\WindowsPowerShell\\v1.0\\powershell.exe",
65+
"command": "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
6666
"presentation": {
6767
"echo": true,
6868
"reveal": "always",
@@ -86,7 +86,7 @@
8686
},
8787
{
8888
"label": "Package",
89-
"command": "c:\\windows\\system32\\WindowsPowerShell\\v1.0\\powershell.exe",
89+
"command": "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
9090
"group": "none",
9191
"presentation": {
9292
"echo": true,
@@ -114,7 +114,7 @@
114114
},
115115
{
116116
"label": "Test",
117-
"command": "c:\\windows\\system32\\WindowsPowerShell\\v1.0\\powershell.exe",
117+
"command": "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
118118
"group": "test",
119119
"presentation": {
120120
"echo": true,
@@ -138,7 +138,7 @@
138138
},
139139
{
140140
"label": "Docs",
141-
"command": "c:\\windows\\system32\\WindowsPowerShell\\v1.0\\powershell.exe",
141+
"command": "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
142142
"presentation": {
143143
"echo": true,
144144
"reveal": "always",

CSharp/TfsCmdlets.Common/Cmdlets/CmdletBase.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Reflection;
22
using System.Runtime.Versioning;
3+
using Newtonsoft.Json.Linq;
34
using TfsCmdlets.Util;
45

56
namespace TfsCmdlets.Cmdlets
@@ -41,7 +42,7 @@ public CmdletBase()
4142
/// Returns the PowerShell command name of this cmdlet
4243
/// </summary>
4344
/// <value>The name of the this, as defined by the [Cmdlet] attribute. If the attribute is missing, returns the class name.</value>
44-
public virtual string DisplayName
45+
public virtual string CmdletDisplayName
4546
{
4647
get
4748
{
@@ -110,7 +111,19 @@ protected virtual void DoProcessRecord()
110111

111112
if (!ReturnsValue || result == null) continue;
112113

113-
WriteObject(result, true);
114+
switch (result)
115+
{
116+
case JToken j:
117+
{
118+
WriteObject(PSJsonConverter.Deserialize(j.ToString(), true), true);
119+
break;
120+
}
121+
default:
122+
{
123+
WriteObject(result, true);
124+
break;
125+
}
126+
}
114127
}
115128
}
116129
catch (PipelineStoppedException)
@@ -151,7 +164,7 @@ private void InjectDependencies()
151164

152165
private void LogParameters()
153166
{
154-
Logger.LogParameters(DisplayName, Parameters);
167+
Logger.LogParameters(CmdletDisplayName, Parameters);
155168
}
156169

157170
private string GetVerb()

CSharp/TfsCmdlets.Common/Controllers/ControllerBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public abstract class ControllerBase : IController
66

77
public string Noun => GetType().Name.Substring(Verb.Length, GetType().Name.EndsWith("Controller") ? GetType().Name.Length - Verb.Length - 10 : GetType().Name.Length - Verb.Length);
88

9-
public string DisplayName => $"{Verb}-Tfs{Noun}";
9+
public string CmdletDisplayName => $"{Verb}-Tfs{Noun}";
1010

1111
public string CommandName => $"{Verb}{Noun}";
1212

@@ -36,7 +36,7 @@ public IEnumerable InvokeCommand()
3636
{
3737
CacheParameters();
3838

39-
Logger.LogParameters(DisplayName, Parameters);
39+
Logger.LogParameters(CmdletDisplayName, Parameters);
4040

4141
return Run();
4242
}

CSharp/TfsCmdlets.Common/Enums.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,56 @@ public enum QueryItemScope
226226
/// </summary>
227227
Both = 3
228228
}
229+
230+
231+
/// <summary>
232+
/// License types available for Azure DevOps users.
233+
/// </summary>
234+
public enum AccountLicenseType
235+
{
236+
/// <summary>
237+
/// "Basic" license
238+
/// </summary>
239+
Basic,
240+
241+
/// <summary>
242+
/// "Basic + Test Plans" license
243+
/// </summary>
244+
BasicTestPlans,
245+
246+
/// <summary>
247+
/// "Stakeholder" (free) license
248+
/// </summary>
249+
Stakeholder,
250+
251+
/// <summary>
252+
/// "Visual Studio Subscriber" (Pro, Enterprise) license
253+
/// </summary>
254+
VisualStudio
255+
}
256+
257+
/// <summary>
258+
/// Represents the type of group entitlement.
259+
/// </summary>
260+
public enum GroupEntitlementType {
261+
/// <summary>
262+
/// Represents an administrator role.
263+
/// </summary>
264+
Administrator,
265+
266+
/// <summary>
267+
/// Represents a contributor.
268+
/// </summary>
269+
Contributor,
270+
271+
/// <summary>
272+
/// Represents a reader.
273+
/// </summary>
274+
Reader,
275+
276+
/// <summary>
277+
/// Represents a stakeholder.
278+
/// </summary>
279+
Stakeholder
280+
}
229281
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Newtonsoft.Json.Linq;
2+
using System.Net.Http; //.HttpResponseMessage
3+
4+
namespace TfsCmdlets.Extensions {
5+
public static class JsonExtensions {
6+
7+
public static JObject ToJsonObject(this string self)
8+
{
9+
return Newtonsoft.Json.JsonConvert.DeserializeObject<JObject>(self);
10+
}
11+
12+
public static T ToJsonObject<T>(this string self)
13+
{
14+
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(self);
15+
}
16+
17+
public static JObject ToJsonObject(this HttpResponseMessage self)
18+
{
19+
var responseBody = self.Content.ReadAsStringAsync().GetAwaiter().GetResult();
20+
return ToJsonObject<JObject>(responseBody);
21+
}
22+
23+
public static T ToJsonObject<T>(this HttpResponseMessage self)
24+
{
25+
var responseBody = self.Content.ReadAsStringAsync().GetAwaiter().GetResult();
26+
return ToJsonObject<T>(responseBody);
27+
}
28+
}
29+
}

CSharp/TfsCmdlets.Common/Extensions/StringExtensions.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Management.Automation;
2+
using Newtonsoft.Json.Linq;
23

34
namespace TfsCmdlets.Extensions
45
{
@@ -32,10 +33,5 @@ public static int FindIndex(this string input, Predicate<char> predicate, int st
3233

3334
return -1;
3435
}
35-
36-
public static T ToJsonObject<T>(this string self)
37-
{
38-
return (T) Newtonsoft.Json.JsonConvert.DeserializeObject(self);
39-
}
4036
}
4137
}

CSharp/TfsCmdlets.Common/Services/IRestApiService.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,53 @@ namespace TfsCmdlets.Services
77
{
88
public interface IRestApiService
99
{
10+
/// <summary>
11+
/// Invokes an Azure DevOps REST API based on a template URL.
12+
/// </summary>
13+
/// <description>
14+
/// This method is used to invoke an Azure DevOps REST API based on the template URL format used in the Azure DevOps REST API documentation.
15+
/// </description>
16+
/// <param name="connection">The connection information.</param>
17+
/// <param name="apiTemplate">The API template.</param>
18+
/// <param name="body">The request body (optional).</param>
19+
/// <param name="method">The HTTP method (default is "GET").</param>
20+
/// <param name="queryParameters">The query parameters (optional).</param>
21+
/// <param name="requestContentType">The request content type (default is "application/json").</param>
22+
/// <param name="responseContentType">The response content type (default is "application/json").</param>
23+
/// <param name="additionalHeaders">Additional headers to include in the request (optional).</param>
24+
/// <param name="apiVersion">The API version (default is "4.1").</param>
25+
/// <param name="project">A delegate that returns the TeamProject, used only when there is a {project}/{projectId} parameter in the template URL (optional).</param>
26+
/// <param name="team">A function that returns the Team, used only when there is a {team}/{teamId} parameter in the template URL (optional).</param>
27+
/// <param name="customServiceHost">The custom service host (optional).</param>
28+
/// <returns>A task representing the asynchronous operation.</returns>
29+
Task<HttpResponseMessage> InvokeTemplateAsync(
30+
Models.Connection connection,
31+
string apiTemplate,
32+
string body = null,
33+
string method = "GET",
34+
IDictionary queryParameters = null,
35+
string requestContentType = "application/json",
36+
string responseContentType = "application/json",
37+
Dictionary<string, string> additionalHeaders = null,
38+
string apiVersion = "4.1",
39+
Func<WebApiTeamProject> project = null,
40+
Func<Models.Team> team = null,
41+
string customServiceHost = null);
42+
43+
/// <summary>
44+
/// Invokes an Azure DevOps REST API endpoint asynchronously.
45+
/// </summary>
46+
/// <param name="connection">The connection information.</param>
47+
/// <param name="path">The path of the API endpoint.</param>
48+
/// <param name="method">The HTTP method to use (default is "GET").</param>
49+
/// <param name="body">The request body (optional).</param>
50+
/// <param name="requestContentType">The content type of the request (default is "application/json").</param>
51+
/// <param name="responseContentType">The expected content type of the response (default is "application/json").</param>
52+
/// <param name="additionalHeaders">Additional headers to include in the request (optional).</param>
53+
/// <param name="queryParameters">Query parameters to include in the request (optional).</param>
54+
/// <param name="apiVersion">The version of the API to use (default is "4.1").</param>
55+
/// <param name="serviceHostName">The host name of the service (optional).</param>
56+
/// <returns>A task representing the asynchronous operation, which returns the HTTP response message.</returns>
1057
Task<HttpResponseMessage> InvokeAsync(
1158
Models.Connection connection,
1259
string path,
@@ -19,6 +66,21 @@ Task<HttpResponseMessage> InvokeAsync(
1966
string apiVersion = "4.1",
2067
string serviceHostName = null);
2168

69+
/// <summary>
70+
/// Invokes an Azure DevOps REST API asynchronously.
71+
/// </summary>
72+
/// <typeparam name="T">The type of the response object.</typeparam>
73+
/// <param name="connection">The connection information.</param>
74+
/// <param name="path">The path of the API endpoint.</param>
75+
/// <param name="method">The HTTP method to use (default is "GET").</param>
76+
/// <param name="body">The request body (default is null).</param>
77+
/// <param name="requestContentType">The content type of the request (default is "application/json").</param>
78+
/// <param name="responseContentType">The content type of the response (default is "application/json").</param>
79+
/// <param name="additionalHeaders">Additional headers to include in the request (default is null).</param>
80+
/// <param name="queryParameters">Query parameters to include in the request (default is null).</param>
81+
/// <param name="apiVersion">The version of the API to use (default is "4.1").</param>
82+
/// <param name="serviceHostName">The host name of the service (default is null).</param>
83+
/// <returns>A task representing the asynchronous operation, which returns the response object.</returns>
2284
Task<T> InvokeAsync<T>(
2385
Models.Connection connection,
2486
string path,

CSharp/TfsCmdlets.Common/Services/Impl/PowerShellServiceImpl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public CmdletBase CurrentCmdlet
3939
}
4040
}
4141

42-
public string CurrentCommand => CurrentCmdlet.DisplayName;
42+
public string CurrentCommand => CurrentCmdlet.CmdletDisplayName;
4343

4444
public string CurrentCommandLine => CurrentCmdlet.MyInvocation.Line.Trim();
4545

0 commit comments

Comments
 (0)