├── .gitignore ├── App.config ├── CreateFieldViewModel.cs ├── Helper ├── ConfigHelper.cs └── ConsoleTable.cs ├── Http ├── DestroyWorkItemsResponse.cs └── GetPlansResponse.cs ├── LICENSE ├── Models ├── Config.cs └── Plan.cs ├── Program.cs ├── Properties └── AssemblyInfo.cs ├── README.md ├── Repos ├── DeliveryPlans.cs ├── Fields.cs ├── Process.cs ├── Projects.cs ├── RecycleBin.cs ├── Tags.cs ├── Teams.cs ├── WorkItemTypes.cs └── WorkItems.cs ├── ViewModels ├── FieldsPerProcess.cs └── PlanList.cs ├── ado-admin.csproj ├── ado-admin.sln ├── config.json └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | /config.debug.json 332 | -------------------------------------------------------------------------------- /App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /CreateFieldViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace adoProcess.ViewModels 8 | { 9 | public class CreateFieldViewModel 10 | { 11 | public string name { get; set; } 12 | public string type { get ; set; } 13 | public string refName { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Helper/ConfigHelper.cs: -------------------------------------------------------------------------------- 1 | using adoAdmin.Models; 2 | using Microsoft.VisualStudio.Services.Organization.Client; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace adoAdmin.Helper 12 | { 13 | public class ConfigHelper 14 | { 15 | private string _organization = ""; 16 | private string _personalAccessToken = ""; 17 | 18 | public ConfigHelper() 19 | { 20 | Init(); 21 | } 22 | 23 | private void Init() 24 | { 25 | string jsonFilePath = @"config.json"; 26 | 27 | try 28 | { 29 | Config config = JsonConvert.DeserializeObject(File.ReadAllText(jsonFilePath)); 30 | 31 | this._organization = config.organization; 32 | this._personalAccessToken = config.personalaccesstoken; 33 | } 34 | catch(Exception) 35 | { 36 | this._organization = string.Empty; 37 | this._personalAccessToken = string.Empty; 38 | } 39 | } 40 | 41 | public string Organization { 42 | get { return _organization; } 43 | } 44 | 45 | public string PersonalAccessToken { 46 | get { return _personalAccessToken; } 47 | } 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Helper/ConsoleTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace adoAdmin.Helper.ConsoleTable 9 | { 10 | public class ConsoleTable 11 | { 12 | public IList Columns { get; set; } 13 | public IList Rows { get; protected set; } 14 | 15 | public ConsoleTableOptions Options { get; protected set; } 16 | 17 | public ConsoleTable(params string[] columns) 18 | : this(new ConsoleTableOptions { Columns = new List(columns) }) 19 | { 20 | } 21 | 22 | public ConsoleTable(ConsoleTableOptions options) 23 | { 24 | Options = options ?? throw new ArgumentNullException("options"); 25 | Rows = new List(); 26 | Columns = new List(options.Columns); 27 | } 28 | 29 | public ConsoleTable AddRow(params object[] values) 30 | { 31 | if (values == null) 32 | throw new ArgumentNullException(nameof(values)); 33 | 34 | if (!Columns.Any()) 35 | throw new Exception("Please set the columns first"); 36 | 37 | if (Columns.Count != values.Length) 38 | throw new Exception( 39 | $"The number columns in the row ({Columns.Count}) does not match the values ({values.Length}"); 40 | 41 | Rows.Add(values); 42 | return this; 43 | } 44 | 45 | public override string ToString() 46 | { 47 | var builder = new StringBuilder(); 48 | 49 | // find the longest column by searching each row 50 | var columnLengths = ColumnLengths(); 51 | 52 | // create the string format with padding 53 | var format = Enumerable.Range(0, Columns.Count) 54 | .Select(i => " | {" + i + ",-" + columnLengths[i] + "}") 55 | .Aggregate((s, a) => s + a) + " |"; 56 | 57 | // find the longest formatted line 58 | var maxRowLength = Math.Max(0, Rows.Any() ? Rows.Max(row => string.Format(format, row).Length) : 0); 59 | var columnHeaders = string.Format(format, Columns.ToArray()); 60 | 61 | // longest line is greater of formatted columnHeader and longest row 62 | var longestLine = Math.Max(maxRowLength, columnHeaders.Length); 63 | 64 | // add each row 65 | var results = Rows.Select(row => string.Format(format, row)).ToList(); 66 | 67 | // create the divider 68 | var divider = " " + string.Join("", Enumerable.Repeat("-", longestLine - 1)) + " "; 69 | 70 | builder.AppendLine(divider); 71 | builder.AppendLine(columnHeaders); 72 | 73 | foreach (var row in results) 74 | { 75 | builder.AppendLine(divider); 76 | builder.AppendLine(row); 77 | } 78 | 79 | builder.AppendLine(divider); 80 | 81 | if (Options.EnableCount) 82 | { 83 | builder.AppendLine(""); 84 | builder.AppendFormat(" Count: {0}", Rows.Count); 85 | } 86 | 87 | return builder.ToString(); 88 | } 89 | 90 | private List ColumnLengths() 91 | { 92 | var columnLengths = Columns 93 | .Select((t, i) => Rows.Select(x => x[i]) 94 | .Union(new[] { Columns[i] }) 95 | .Where(x => x != null) 96 | .Select(x => x.ToString().Length).Max()) 97 | .ToList(); 98 | return columnLengths; 99 | } 100 | 101 | public void Write() 102 | { 103 | Console.WriteLine(ToString()); 104 | } 105 | } 106 | 107 | public class ConsoleTableOptions 108 | { 109 | public IEnumerable Columns { get; set; } = new List(); 110 | public bool EnableCount { get; set; } = true; 111 | } 112 | } -------------------------------------------------------------------------------- /Http/DestroyWorkItemsResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace adoAdmin.Http 4 | { 5 | public class DestroyWorkItemsResponse : IDestroyWorkItemsResponse 6 | { 7 | public bool Success { get; set; } = false; 8 | public HttpStatusCode StatusCode { get; set; } 9 | public string Message { get; set; } 10 | } 11 | 12 | public interface IDestroyWorkItemsResponse 13 | { 14 | bool Success { get; set; } 15 | HttpStatusCode StatusCode { get; set; } 16 | string Message { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Http/GetPlansResponse.cs: -------------------------------------------------------------------------------- 1 | using adoAdmin.ViewModels; 2 | using System.Net; 3 | 4 | namespace adoAdmin.Http 5 | { 6 | public class GetPlansResponse : IGetPlansResponse 7 | { 8 | public bool Success { get; set; } = false; 9 | public HttpStatusCode StatusCode { get; set; } 10 | public string Message { get; set; } 11 | public PlanList Plans { get; set;} 12 | } 13 | 14 | public interface IGetPlansResponse 15 | { 16 | bool Success { get; set; } 17 | HttpStatusCode StatusCode { get; set; } 18 | string Message { get; set; } 19 | PlanList Plans { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dan Hellem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Models/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace adoAdmin.Models 8 | { 9 | public class Config 10 | { 11 | public string organization { get; set; } 12 | public string personalaccesstoken { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Models/Plan.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace adoAdmin.Models 5 | { 6 | public class Plan 7 | { 8 | public string id { get; set; } 9 | public string name { get; set; } 10 | public string description { get; set; } 11 | public DateTime createdDate { get; set; } 12 | public DateTime? lastAccessed { get; set; } 13 | public DateTime? modifiedDate { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using adoAdmin.Helper; 2 | using adoAdmin.Helper.ConsoleTable; 3 | using adoAdmin.Models; 4 | using adoAdmin.ViewModels; 5 | using adoAdmin.Http; 6 | using Microsoft.TeamFoundation.Core.WebApi; 7 | using Microsoft.TeamFoundation.Work.WebApi; 8 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.Models; 9 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; 10 | using Microsoft.VisualStudio.Services.Common; 11 | using Microsoft.VisualStudio.Services.WebApi; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Linq; 15 | using System.Threading; 16 | 17 | namespace adoAdmin 18 | { 19 | public class Program 20 | { 21 | public static int Main(string[] args) 22 | { 23 | 24 | if (args.Length == 0) 25 | { 26 | ShowUsage(); 27 | return 0; 28 | } 29 | 30 | args = SetArgumentsFromConfig(args); 31 | 32 | string org, pat, process, project, refname, name, type, action; 33 | string witrefname, targetprocess; 34 | int days; 35 | bool _deleteOverRide = false; 36 | 37 | try 38 | { 39 | CheckArguments(args, out org, out pat, out project, out refname, out name, out type, out action, out process, out witrefname, out targetprocess, out days); 40 | 41 | Uri baseUri = new Uri(org); 42 | 43 | VssCredentials clientCredentials = new VssCredentials(new VssBasicCredential("username", pat)); 44 | VssConnection vssConnection = new VssConnection(baseUri, clientCredentials); 45 | 46 | // close work item type 47 | if (action == "clonewit") 48 | { 49 | Console.WriteLine("Start Validation..."); 50 | 51 | bool val = CloneWitAndProcessValidation(vssConnection, process, targetprocess, witrefname); 52 | 53 | if (! val) return 0; 54 | } 55 | 56 | // action out all fields 57 | if (action == "listallfields") 58 | { 59 | List fields = Repos.Fields.GetAllFields(vssConnection); 60 | 61 | var table = new ConsoleTable("Name", "Reference Name", "Type"); 62 | 63 | foreach (WorkItemField field in fields) 64 | { 65 | table.AddRow(field.Name, field.ReferenceName, field.Type); 66 | } 67 | 68 | table.Write(); 69 | Console.WriteLine(); 70 | 71 | return 0; 72 | } 73 | 74 | // action out all fields 75 | if (action == "allpicklists") 76 | { 77 | Console.Write("Loading all picklists and fields: "); 78 | 79 | List fields = Repos.Fields.GetAllFields(vssConnection); 80 | List picklists = Repos.Process.ListPicklists(vssConnection); 81 | 82 | Console.Write("Done"); 83 | Console.WriteLine(""); 84 | 85 | var table = new ConsoleTable("Name", "Id", "Type", "Fields"); 86 | 87 | foreach (PickListMetadata item in picklists) 88 | { 89 | string fieldName = string.Empty; 90 | var field = fields.Where(x => x.IsPicklist == true && x.PicklistId == item.Id); 91 | fieldName = field.Count() > 0 ? field.FirstOrDefault().Name : String.Empty; 92 | 93 | table.AddRow(item.Name, item.Id, item.Type, fieldName).Write(); 94 | } 95 | 96 | table.Write(); 97 | Console.WriteLine(); 98 | 99 | return 0; 100 | } 101 | 102 | // action out all fields 103 | if (action == "picklistswithnofield") 104 | { 105 | Console.Write(" Loading all picklists and fields: "); 106 | 107 | List fields = Repos.Fields.GetAllFields(vssConnection); 108 | List picklists = Repos.Process.ListPicklists(vssConnection); 109 | 110 | Console.WriteLine("Done"); 111 | 112 | var table = new ConsoleTable("Name", "Id", "Type"); 113 | 114 | // look through all the picklists and lets see if they are attached to any fields 115 | foreach (PickListMetadata item in picklists) 116 | { 117 | string fieldName = string.Empty; 118 | var field = fields.Where(x => x.IsPicklist == true && x.PicklistId == item.Id); 119 | 120 | // found one 121 | if (field.Count() == 0) 122 | { 123 | table.AddRow(item.Name, item.Id, item.Type); 124 | } 125 | } 126 | 127 | // check to see if we have any results 128 | if (table.Rows.Count == 0) 129 | { 130 | Console.WriteLine(); 131 | Console.WriteLine(" No unused picklists found."); 132 | return 0; 133 | } 134 | 135 | Console.WriteLine(); 136 | Console.WriteLine(" These picklists are not being used by any fields:"); 137 | 138 | table.Write(); 139 | 140 | // if we have some results, do we want to delete the picklists? 141 | if (table.Rows.Count > 0) 142 | { 143 | Console.WriteLine(); 144 | Console.WriteLine(" Would you like to delete these unused picklists?"); 145 | Console.WriteLine(); 146 | Console.WriteLine(" Press 'Y' continue or 'N' to abort."); 147 | 148 | // no i don't 149 | if (Console.ReadKey().Key == ConsoleKey.N) 150 | { 151 | Console.WriteLine(" Delete aborted!"); 152 | return 0; 153 | } 154 | 155 | // yes i do 156 | if (Console.ReadKey().Key == ConsoleKey.Y) 157 | { 158 | Console.Write(" Deleting unused picklists: "); 159 | 160 | // loop throuh the table and delete a picklist one at a time 161 | foreach (IList row in table.Rows) 162 | { 163 | Repos.Process.DeletePicklist(vssConnection, row[1].ToString()); 164 | } 165 | 166 | Console.Write("Done"); 167 | Console.WriteLine(" "); 168 | } 169 | } 170 | 171 | return 0; 172 | } 173 | 174 | // get one field by refname and me all of the processes that field is in 175 | if (action == "getfield" && (! String.IsNullOrEmpty(refname))) 176 | { 177 | List processList = Repos.Process.GetProcesses(vssConnection); 178 | List witList; 179 | 180 | var table = new ConsoleTable("Process", "Work Item Type", "Field Name", "Field Reference Name"); 181 | 182 | foreach (var processInfo in processList) 183 | { 184 | witList = Repos.Process.GetWorkItemTypes(vssConnection, processInfo.TypeId); 185 | 186 | foreach(var witItem in witList) 187 | { 188 | ProcessWorkItemTypeField witField = Repos.Process.GetField(vssConnection, processInfo.TypeId, witItem.ReferenceName, refname); 189 | 190 | if (witField != null) 191 | { 192 | table.AddRow(processInfo.Name, witItem.Name, witField.Name, witField.ReferenceName); 193 | } 194 | 195 | witField = null; 196 | } 197 | } 198 | 199 | table.Write(); 200 | Console.WriteLine(); 201 | } 202 | 203 | // list of projects and work item types the field is used in 204 | if (action == "getfieldforprojects" && (!String.IsNullOrEmpty(refname))) 205 | { 206 | Console.WriteLine("Getting list of projects and work item types..."); 207 | Console.WriteLine(); 208 | 209 | var table = new ConsoleTable("Project", "Work Item Type", "Field Reference Name", "Field Name"); 210 | WorkItemTypeFieldWithReferences field; 211 | 212 | List projectList = Repos.Projects.GetAllProjects(vssConnection); 213 | List witList = null; 214 | 215 | foreach (TeamProjectReference projectItem in projectList) 216 | { 217 | witList = Repos.WorkItemTypes.GetWorkItemTypesForProject(vssConnection, projectItem.Name); 218 | 219 | foreach (WorkItemType witItem in witList) 220 | { 221 | field = Repos.Fields.GetFieldForWorkItemType(vssConnection, projectItem.Name, witItem.ReferenceName, refname); 222 | 223 | if (field != null) 224 | { 225 | table.AddRow(projectItem.Name, witItem.ReferenceName, field.ReferenceName, field.Name); 226 | } 227 | 228 | field = null; 229 | } 230 | } 231 | 232 | table.Write(); 233 | Console.WriteLine(); 234 | 235 | field = null; 236 | table = null; 237 | witList = null; 238 | } 239 | 240 | // search for a specific field by refname to see if it exists 241 | if (action == "searchfields") 242 | { 243 | var fields = Repos.Fields.SearchFields(vssConnection, name, type); 244 | 245 | if (fields.Count == 0) 246 | { 247 | Console.WriteLine("No fields found for name: '" + name + "' or type: '" + type + "'"); 248 | return 0; 249 | } 250 | 251 | var table = new ConsoleTable("Name", "Reference Name", "Type"); 252 | 253 | foreach (WorkItemField field in fields) 254 | { 255 | table.AddRow(field.Name, field.ReferenceName, field.Type); 256 | } 257 | 258 | table.Write(); 259 | Console.WriteLine(); 260 | 261 | return 0; 262 | } 263 | 264 | // add new field to the organization 265 | if (action == "addfield") 266 | { 267 | //check to see if the type is a legit type 268 | int pos = Array.IndexOf(Repos.Fields.Types, type); 269 | 270 | if (pos == -1) 271 | { 272 | var types = Repos.Fields.Types; 273 | 274 | Console.WriteLine("Invalid field type value '" + type + "'"); 275 | Console.WriteLine(); 276 | Console.WriteLine("Valid field types are:"); 277 | Console.WriteLine(); 278 | 279 | foreach (string item in types) 280 | { 281 | Console.WriteLine(item); 282 | } 283 | 284 | return 0; 285 | } 286 | 287 | //check and make sure the field does not yet exist 288 | var field = Repos.Fields.GetField(vssConnection, refname); 289 | 290 | if (field != null) 291 | { 292 | Console.WriteLine("Field already exists"); 293 | Console.WriteLine(); 294 | 295 | var table = new ConsoleTable("Name", "Reference Name", "Type"); 296 | 297 | table.AddRow(field.Name, field.ReferenceName, field.Type); 298 | 299 | table.Write(); 300 | Console.WriteLine(); 301 | 302 | return 0; 303 | } 304 | 305 | WorkItemField newField = Repos.Fields.AddField(vssConnection, refname, name, type); 306 | 307 | if (newField != null) 308 | { 309 | Console.WriteLine("Field '" + refname + "' was successfully added"); 310 | } 311 | } 312 | 313 | // list of fields in a process 314 | if (action == "listfieldsforprocess") 315 | { 316 | List list = Repos.Fields.ListFieldsForProcess(vssConnection, process); 317 | 318 | var table = new ConsoleTable("Work Item Type", "Name", "Reference Name", "Type"); 319 | 320 | foreach (FieldsPerProcess item in list) 321 | { 322 | List fields = item.fields; 323 | 324 | foreach(ProcessWorkItemTypeField field in fields) 325 | { 326 | table.AddRow(item.workItemType.Name, field.Name, field.ReferenceName, field.Type); 327 | } 328 | } 329 | 330 | table.Write(); 331 | Console.WriteLine(); 332 | 333 | return 0; 334 | } 335 | 336 | // action out all fields 337 | if (action == "emptyrecyclebin") 338 | { 339 | Console.Write(" Loading deleted items: "); 340 | 341 | List workitems = Repos.RecycleBin.GetDeletedWorkItemsByWiql(vssConnection, project, days); 342 | 343 | Console.WriteLine(workitems.Count); 344 | 345 | if (workitems.Count < 1) 346 | { 347 | Console.WriteLine($" There are no items in the recycle bin that are ready to be destroyed (-{days} days)."); 348 | Console.WriteLine(" Completed."); 349 | return 0; 350 | } 351 | 352 | Console.WriteLine(" "); 353 | Console.WriteLine(" WARNING!", Console.ForegroundColor = ConsoleColor.Red); 354 | Console.WriteLine(" You are about to permentantly destroy work items. This action cannot be undone.", Console.ForegroundColor = ConsoleColor.Red); 355 | Console.WriteLine(" "); 356 | Console.ResetColor(); 357 | Console.WriteLine(" Press any key to continue or 'N' to abort."); 358 | 359 | if (Console.ReadKey().Key == ConsoleKey.N) return 0; 360 | 361 | // use delete override of batch api is failing (edge case bug) 362 | // this will delete one a time in the current batch 363 | if (_deleteOverRide) 364 | { 365 | Console.WriteLine($" Running Delete Override (one at a time)..."); 366 | 367 | int i = 1; 368 | 369 | // Loop over strings. 370 | foreach (WorkItemReference workitem in workitems) 371 | { 372 | Console.WriteLine($" [{i}] Destroying work item {workitem.Id}"); 373 | Repos.RecycleBin.DestroyWorkItem(vssConnection, workitem.Id); 374 | i = i + 1; 375 | } 376 | 377 | Console.WriteLine(" "); 378 | Console.WriteLine($" Completed set of {workitems.Count}"); 379 | 380 | return 0; 381 | } 382 | 383 | while (workitems.Count > 0) 384 | { 385 | int i = 0; 386 | String[] ids = workitems.Count >= 200 ? new String[200] : new String[workitems.Count]; 387 | 388 | foreach (WorkItemReference workitem in workitems) 389 | { 390 | ids[i] = workitem.Id.ToString(); 391 | i = i + 1; 392 | } 393 | 394 | string val = String.Join(",", ids); 395 | 396 | Console.Write(" Destroying items: "); 397 | 398 | IDestroyWorkItemsResponse response = Repos.RecycleBin.DestroyWorkItems(pat, org, val); 399 | 400 | if (!response.Success) 401 | { 402 | Console.WriteLine("Failed."); 403 | Console.WriteLine(" "); 404 | Console.WriteLine(response.StatusCode); 405 | Console.WriteLine(response.Message); 406 | break; 407 | } 408 | 409 | Console.WriteLine("Success."); 410 | Console.WriteLine(" "); 411 | 412 | Console.Write(" Waiting a moment..."); 413 | Thread.Sleep(2000); 414 | Console.WriteLine("done."); 415 | Console.WriteLine(" "); 416 | 417 | Console.Write(" Loading more deleted items: "); 418 | 419 | workitems = Repos.RecycleBin.GetDeletedWorkItemsByWiql(vssConnection, project, days); 420 | 421 | Console.WriteLine(workitems.Count); 422 | } 423 | 424 | //table.Write(); 425 | Console.WriteLine(" "); 426 | Console.WriteLine(" Complete."); 427 | 428 | return 0; 429 | } 430 | 431 | // list tags and tag usage 432 | if (action == "listemptytags") 433 | { 434 | Console.Write(" Loading tags: "); 435 | 436 | List tags = Repos.Tags.GetAllTags(vssConnection, project); 437 | 438 | Console.Write("Done"); 439 | Console.WriteLine(""); 440 | 441 | var table = new ConsoleTable("Name", "Id"); 442 | 443 | Console.Write(" Looping through each tag to find linked worked items. This may take a while..."); 444 | 445 | foreach (WorkItemTagDefinition tag in tags) 446 | { 447 | List list = Repos.Tags.FetchWorkItemByTag(vssConnection, project, tag.Name); 448 | 449 | if (list.Count == 0) { 450 | table.AddRow(tag.Name, tag.Id); 451 | } 452 | } 453 | 454 | Console.Write("Done"); 455 | Console.WriteLine(""); 456 | Console.WriteLine(""); 457 | Console.WriteLine(" Tags that can be deleted"); 458 | 459 | if (table.Rows .Count > 0 ) { table.Write(); } else { Console.WriteLine(" No empty tags found"); } 460 | 461 | Console.WriteLine(); 462 | } 463 | 464 | // Delete a tag 465 | if (action == "deletetag") 466 | { 467 | Console.Write("Deleting tag: "); 468 | 469 | try 470 | { 471 | Repos.Tags.DeleteTag(vssConnection, project, name); 472 | 473 | Console.WriteLine($"Tag: '" + name + "' deleted successfully."); 474 | } 475 | catch (Exception e) 476 | { 477 | Console.WriteLine($"Failed to delete tag: '" + name + "'."); 478 | if ( e.InnerException is null ) 479 | { 480 | Console.WriteLine(e.Message); 481 | } 482 | else 483 | { 484 | Console.WriteLine(e.InnerException.Message); 485 | } 486 | } 487 | Console.WriteLine(); 488 | } 489 | 490 | if (action == "list-delete-plans") 491 | { 492 | Console.Write(" Loading delivery plans: "); 493 | 494 | ConsoleKeyInfo cki; 495 | List list = new List(); 496 | 497 | IGetPlansResponse response = Repos.DeliveryPlans.GetPlans(pat, org, project); 498 | List plans = response.Plans.value.OrderByDescending(x => x.lastAccessed).ToList(); 499 | 500 | Console.Write("Done"); 501 | Console.WriteLine(""); 502 | 503 | var table = new ConsoleTable("Name", "Description", "Created", "Modified", "Last Accessed"); 504 | 505 | foreach (Models.Plan plan in response.Plans.value) 506 | { 507 | if (plan.lastAccessed.HasValue && plan.lastAccessed.Value.AddDays(days) < DateTime.Today) 508 | { 509 | table.AddRow(plan.name, plan.description, plan.createdDate, plan.modifiedDate, plan.lastAccessed); 510 | list.Add(plan.id); 511 | } 512 | 513 | // get list of plans that have never been accessed 514 | // for those plans that existed before we added last accessed fields 515 | if (!plan.lastAccessed.HasValue) 516 | { 517 | table.AddRow(plan.name, plan.description, plan.createdDate, plan.modifiedDate, "Never"); 518 | list.Add(plan.id); 519 | } 520 | } 521 | 522 | if (table.Rows.Count == 0) 523 | { 524 | Console.WriteLine($" No delivery plans found that have not been accessed in the last {days} days."); 525 | } 526 | else 527 | { 528 | table.Write(); 529 | 530 | Console.WriteLine(); 531 | Console.Write($" Press to delete these {list.Count} plans or press to exit... "); 532 | Console.WriteLine(); 533 | 534 | do 535 | { 536 | cki = Console.ReadKey(); 537 | if (cki.Key == ConsoleKey.Delete) 538 | { 539 | Console.WriteLine(""); 540 | Console.Write(" Deleting plans: "); 541 | Console.WriteLine(""); 542 | 543 | foreach (string id in list) 544 | { 545 | Console.Write($" - {id}"); 546 | Repos.DeliveryPlans.DeletePlan(vssConnection, project, id); 547 | } 548 | 549 | Console.WriteLine(""); 550 | } 551 | } while (cki.Key != ConsoleKey.Enter); 552 | 553 | Console.Write("Done"); 554 | } 555 | 556 | table = null; 557 | response = null; 558 | plans = null; 559 | list = null; 560 | } 561 | 562 | if (action == "mylimits") 563 | { 564 | var table = new ConsoleTable("Project limit", "Status", "Recommendation"); 565 | 566 | if (!string.IsNullOrEmpty(project)) 567 | { 568 | Console.Write("Loading usage data for project limits: "); 569 | 570 | List workitems = Repos.RecycleBin.GetDeletedWorkItemsByWiql(vssConnection, project); 571 | 572 | table.AddRow("Recycle Bin", $"{workitems.Count} deleted work items", workitems.Count > 19000 ? $"Empty recycle bin" : $" - ").Write(); 573 | 574 | workitems = null; 575 | 576 | Console.Write("Done"); 577 | Console.WriteLine(""); 578 | } 579 | 580 | table.Write(); 581 | Console.WriteLine(); 582 | 583 | return 0; 584 | } 585 | 586 | vssConnection = null; 587 | } 588 | catch (ArgumentException ex) 589 | { 590 | Console.WriteLine(ex.Message); 591 | 592 | ShowUsage(); 593 | return -1; 594 | } 595 | 596 | return 0; 597 | } 598 | 599 | private static bool CloneWitAndProcessValidation(VssConnection vssConnection, string process, string targetProcess, string witRefName) 600 | { 601 | List processList = Repos.Process.GetProcesses(vssConnection); 602 | 603 | ProcessInfo sourceProcessInfo = processList.Find(x => x.Name == process); 604 | ProcessInfo targetProcessInfo = processList.Find(x => x.Name == targetProcess); 605 | 606 | Console.Write(" Validating source process '{0}'...", process); 607 | 608 | if (sourceProcessInfo == null) 609 | { 610 | Console.Write("failed (process not found) \n"); 611 | return false; 612 | } 613 | else 614 | { 615 | Console.Write("done \n"); 616 | }; 617 | 618 | Console.Write(" Validating target process '{0}'...", targetProcess); 619 | 620 | if (targetProcessInfo == null) 621 | { 622 | Console.Write("failed (process not found) \n"); 623 | return false; 624 | } 625 | else 626 | { 627 | Console.Write("done \n"); 628 | }; 629 | 630 | Console.Write(" Validating work item type '{0}' exists in source process...", witRefName); 631 | 632 | if (Repos.Process.GetWorkItemType(vssConnection, sourceProcessInfo.TypeId, witRefName) == null) 633 | { 634 | Console.Write("failed (work item type not found) \n"); 635 | return false; 636 | } 637 | else 638 | { 639 | Console.Write("done \n"); 640 | } 641 | 642 | Console.Write(" Validating work item type {0} does not exist in target process...", witRefName); 643 | 644 | if (Repos.Process.GetWorkItemType(vssConnection, targetProcessInfo.TypeId, witRefName) != null) 645 | { 646 | Console.Write("failed (work item type found) \n"); 647 | return false; 648 | } 649 | else 650 | { 651 | Console.Write("done \n"); 652 | } 653 | 654 | return true; 655 | } 656 | 657 | private static void CheckArguments(string[] args, out string org, out string pat, out string project, out string refname, out string name, out string type, out string action, out string process, out string witrefname, out string targetprocess, out int days) 658 | { 659 | org = null; 660 | refname = null; 661 | name = null; 662 | type = null; 663 | action = null; 664 | project = null; 665 | pat = null; 666 | process = null; 667 | witrefname = null; 668 | targetprocess = null; 669 | days = 0; 670 | 671 | //Dictionary argsMap = new Dictionary(); 672 | 673 | foreach (var arg in args) 674 | { 675 | if (arg[0] == '/' && arg.IndexOf(':') > 1) 676 | { 677 | string key = arg.Substring(1, arg.IndexOf(':') - 1); 678 | string value = arg.Substring(arg.IndexOf(':') + 1); 679 | 680 | switch (key) 681 | { 682 | case "org": 683 | org = "https://dev.azure.com/" + value; 684 | break; 685 | case "pat": 686 | pat = value; 687 | break; 688 | case "project": 689 | project = value; 690 | break; 691 | case "refname": 692 | refname = value; 693 | break; 694 | case "name": 695 | name = value; 696 | break; 697 | case "type": 698 | type = value; 699 | break; 700 | case "action": 701 | action = value; 702 | break; 703 | case "process": 704 | process = value; 705 | break; 706 | case "witrefname": 707 | witrefname = value; 708 | break; 709 | case "targetprocess": 710 | targetprocess = value; 711 | break; 712 | case "days": 713 | days = Convert.ToInt32(value); 714 | break; 715 | default: 716 | throw new ArgumentException("Unknown argument", key); 717 | } 718 | } 719 | } 720 | 721 | if (org == null || pat == null) 722 | { 723 | throw new ArgumentException("Missing required arguments"); 724 | } 725 | 726 | if ((action == "getfield") && string.IsNullOrEmpty(refname)) 727 | { 728 | throw new ArgumentException("getfield action requires refname value"); 729 | } 730 | 731 | if ((action == "getfieldforprojects") && string.IsNullOrEmpty(refname)) 732 | { 733 | throw new ArgumentException("getfieldforprojects action requires field refname value"); 734 | } 735 | 736 | if ((action == "addfield") && (string.IsNullOrEmpty(refname) || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(type))) 737 | { 738 | throw new ArgumentException("addfield action requires refname, name, and type value"); 739 | } 740 | 741 | if ((action == "searchfield" && string.IsNullOrEmpty(name) && string.IsNullOrEmpty(refname))) 742 | { 743 | throw new ArgumentException("searchfield action requires name or type value"); 744 | } 745 | 746 | if (action == "listfieldsforprocess" && string.IsNullOrEmpty(process)) 747 | { 748 | throw new ArgumentException("listfieldsforprocess action requires process"); 749 | } 750 | 751 | if (action == "clonewit") { 752 | 753 | if (process == null) 754 | { 755 | throw new ArgumentException("Missing required argument 'process'"); 756 | } 757 | 758 | if (witrefname == null) 759 | { 760 | throw new ArgumentException("Missing required argument 'witrefname' for the work item type you want to clone"); 761 | } 762 | 763 | if (targetprocess == null) 764 | { 765 | throw new ArgumentException("Missing required argument 'targetprocess' for the process you want to clone the work item type into"); 766 | } 767 | } 768 | 769 | if (action == "emptyrecyclebin" && string.IsNullOrEmpty(project)) 770 | { 771 | throw new ArgumentException("Missing required argument 'project'"); 772 | } 773 | 774 | if (action == "listemptytags" && string.IsNullOrEmpty(project)) 775 | { 776 | throw new ArgumentException("Missing required argument 'project'"); 777 | } 778 | 779 | if (action == "deletetag") 780 | { 781 | if (string.IsNullOrEmpty(project)) 782 | { 783 | throw new ArgumentException("Missing required argument 'project'"); 784 | } 785 | 786 | if (string.IsNullOrEmpty(name)) 787 | { 788 | throw new ArgumentException("Missing required argument 'name'"); 789 | } 790 | } 791 | 792 | if (action == "list-delete-plans" && days == 0) 793 | { 794 | throw new ArgumentException("Missing required argument 'days'"); 795 | } 796 | 797 | if (action == "list-delete-plans" && string.IsNullOrEmpty(project)) 798 | { 799 | throw new ArgumentException("Missing required argument 'project'"); 800 | } 801 | } 802 | 803 | private static string[] SetArgumentsFromConfig (string[] args) 804 | { 805 | var configHelper = new ConfigHelper(); 806 | bool org = false; 807 | bool pat = false; 808 | 809 | foreach (var arg in args) 810 | { 811 | if (arg[0] == '/' && arg.IndexOf(':') > 1) 812 | { 813 | string key = arg.Substring(1, arg.IndexOf(':') - 1); 814 | string value = arg.Substring(arg.IndexOf(':') + 1); 815 | 816 | switch (key) 817 | { 818 | case "org": 819 | org = true; 820 | break; 821 | case "pat": 822 | pat = true; 823 | break; 824 | default: 825 | break; 826 | } 827 | } 828 | } 829 | 830 | if (! org && !String.IsNullOrEmpty(configHelper.Organization)) 831 | { 832 | Array.Resize(ref args, args.Length + 1); 833 | args[args.Length - 1] = "/org:" + configHelper.Organization; 834 | } 835 | 836 | if (!pat && !String.IsNullOrEmpty(configHelper.PersonalAccessToken)) 837 | { 838 | Array.Resize(ref args, args.Length + 1); 839 | args[args.Length - 1] = "/pat:" + configHelper.PersonalAccessToken; 840 | } 841 | 842 | configHelper = null; 843 | 844 | return args; 845 | } 846 | 847 | private static void ShowUsage() 848 | { 849 | Console.WriteLine("CLI to manage an inherited process in Azure DevOps. View Readme.MD in GitHub repo for full details."); 850 | Console.WriteLine(""); 851 | Console.WriteLine("Arguments:"); 852 | Console.WriteLine(""); 853 | Console.WriteLine(" /org:{value} azure devops organization name"); 854 | Console.WriteLine(" /pat:{value} personal access token"); 855 | Console.WriteLine(""); 856 | Console.WriteLine(" /action: listallfields, getfieldforprojects, addfield, searchfield, listfieldsforprocess, allpicklists, picklistswithnofield, clonewit"); 857 | Console.WriteLine(" /refname:{value} refname of field getting or adding"); 858 | Console.WriteLine(" /name:{value} field friendly name"); 859 | Console.WriteLine(" /process:{value} name of process"); 860 | Console.WriteLine(" /type:{value} type field creating"); 861 | Console.WriteLine(" /targetprocess:{value} target process for where you want to clone a wit into"); 862 | 863 | Console.WriteLine(""); 864 | Console.WriteLine("Examples:"); 865 | Console.WriteLine(""); 866 | Console.WriteLine(" /org:fabrikam /pat:{value} /action:listemptytags /project:projectname"); 867 | Console.WriteLine(" /org:fabrikam /pat:{value} /action:listallfields"); 868 | Console.WriteLine(" /org:fabrikam /pat:{value} /action:allpicklists"); 869 | Console.WriteLine(" /org:fabrikam /pat:{value} /action:picklistswithnofield"); 870 | Console.WriteLine(" /org:fabrikam /pat:{value} /action:getfield /refname:System.Title"); 871 | Console.WriteLine(" /org:fabrikam /pat:{value} /action:listfieldsforprocess /process:Agile"); 872 | Console.WriteLine(" /org:fabrikam /pat:{value} /action:listfieldsforprocess /process:Agile"); 873 | Console.WriteLine(" /org:fabrikam /pat:{value} /action:clonewit /process:sourceprocess /witrefname:custom.ticket /targetprocess:targetprocess"); 874 | 875 | Console.WriteLine(""); 876 | } 877 | } 878 | } 879 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ado-process")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ado-process")] 13 | [assembly: AssemblyCopyright("Copyright © 2019")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("38b89549-ec6d-432e-b3dc-f7fbdb9715fd")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # azure-devops-admin-cli 2 | CLI to manage work item admin tasks in Azure DevOps 3 | 4 | ## Building and running 5 | 6 | 1. Open solution in Visual Studio and build the project. 7 | 2. Open bin > release folder (or debug) to find witadmin.exe 8 | 3. Run command line ```adoadmin.exe``` 9 | 4. Add arguments below to run specific commands 10 | 11 | #### Example 12 | 13 | ``` 14 | adoadmin.exe /org:myorgname /pat:1461fe40a1074619b1b29438ad19c71b /action:listallfields 15 | ``` 16 | 17 | ## Arguments 18 | 19 | ``` 20 | /org:{value} azure devops organization name 21 | /pat:{value} personal access token 22 | 23 | /action:{value} listallfields, getfield, addfield, listfieldsforprocess, allpicklists, picklistswithnofield, emptyrecyclebin 24 | /refname:{value} refname of field getting or adding 25 | /name:{value} field friendly name 26 | /type:{value} type field creating 27 | 28 | /days:{value} used with emptyrecyclebin and list-delete-plans action. Number of days in the past from today 29 | ``` 30 | 31 | ## 📃Process 32 | 33 | ``` 34 | listallfields lists all fields in the organization 35 | getfield get a specific field by refname 36 | addfield add a field 37 | listfieldsforprocess list of fields in a process 38 | searchfields search for a specific field by refname to see if it exists 39 | getfieldforprojects list of projects and work item types the field is used in 40 | allpicklists list all picklists and the field they are associated to 41 | picklistswithnofield picklists that are not being used 42 | list-delete-plans list out and delete (optional) delivery plans that have not been accessed in x number of days 43 | ``` 44 | 45 | ### Examples 46 | 47 | ``` 48 | adoadmin.exe /org:{organization name} /pat:{value} /action:listallfields 49 | adoadmin.exe /org:{organization name} /pat:{value} /action:allpicklists 50 | adoadmin.exe /org:{organization name} /pat:{value} /action:picklistswithnofield 51 | adoadmin.exe /org:{organization name} /pat:{value} /action:listfieldsforprocess /process:Agile 52 | adoadmin.exe /org:{organization name} /pat:{value} /action:getfield /refname:System.Title 53 | ``` 54 | 55 | ## 📅 Delivery Plans 56 | 57 | Clean up Delivery Plans that have not been accessed over a number of days. This can be helpful when trying to stay under the 1,000 plan limit per project. We recommend deleting plans that have not had any activity in the last 3-6 months. 58 | 59 | ``` 60 | list-delete-plans list out and delete (optional) delivery plans that have not been accessed in x number of days 61 | ``` 62 | 63 | ### Example 64 | ``` 65 | adoadmin.exe /org:{organization name} /pat:{value} /action:list-delete-plans /days:182 66 | ``` 67 | 68 | ## ♻️ Recyle bin 69 | 70 | Used to delete work items forever out the recycle bin. 71 | 72 | ``` 73 | adoadmin.exe /org:{organization name} /pat:{value} /action:emptyrecyclebin /project:{project name} /days:0 74 | ``` 75 | 76 | ### Examples 77 | 78 | Empty everything from the recycle bin 79 | ``` 80 | adoadmin.exe /org:{organization name} /pat:{value} /action:emptyrecyclebin /project:{project name} /days:0 81 | ``` 82 | 83 | Empty work items that have not been updated in the last 365 days 84 | ``` 85 | adoadmin.exe /org:{organization name} /pat:{value} /project:{project name} /days:365 86 | ``` 87 | 88 | ## 🏷️ Tags 89 | 90 | Find all the tags that are not used and can be deleted. 91 | 92 | ``` 93 | adoadmin.exe /org:{organization name} /pat:{value} /action:listemptytags /project:{project name} 94 | ``` 95 | 96 | Delete a specific tag in a project. 97 | ``` 98 | adoadmin.exe /org:{organization name} /pat:{value} /action:deletetag /project:{project name} /name:{tag name} 99 | ``` 100 | -------------------------------------------------------------------------------- /Repos/DeliveryPlans.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi; 2 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; 3 | using Microsoft.TeamFoundation.Work.WebApi; 4 | using Microsoft.VisualStudio.Services.WebApi; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using adoAdmin.ViewModels; 8 | using System.IO; 9 | using System.Net.Http.Headers; 10 | using System.Net.Http; 11 | using System.Text; 12 | using System; 13 | using adoAdmin.Http; 14 | using Newtonsoft.Json; 15 | 16 | namespace adoAdmin.Repos 17 | { 18 | public static class DeliveryPlans 19 | { 20 | public static IGetPlansResponse GetPlans(string pat, string organizationUrl, string project) 21 | { 22 | IGetPlansResponse returnResponse = new GetPlansResponse(); 23 | 24 | using (var client = new HttpClient()) 25 | { 26 | //client.BaseAddress = new Uri(org); 27 | client.DefaultRequestHeaders.Accept.Clear(); 28 | client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); 29 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}", "", pat)))); 30 | 31 | HttpResponseMessage response = client.GetAsync($"{organizationUrl}/{project}/_apis/work/plans?api-version=7.1-preview.1").Result; 32 | 33 | string result = response.Content.ReadAsStringAsync().Result; 34 | 35 | returnResponse.StatusCode = response.StatusCode; 36 | returnResponse.Message = "Success"; 37 | returnResponse.Success = response.StatusCode == System.Net.HttpStatusCode.NoContent ? true : false; 38 | returnResponse.Plans = JsonConvert.DeserializeObject(result); 39 | 40 | client.Dispose(); 41 | 42 | return returnResponse; 43 | } 44 | } 45 | 46 | public static void DeletePlan(VssConnection connection, string project, string id) 47 | { 48 | WorkHttpClient client = connection.GetClient(); 49 | 50 | client.DeletePlanAsync(project, id).SyncResult(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Repos/Fields.cs: -------------------------------------------------------------------------------- 1 | using adoAdmin.Helper; 2 | using adoAdmin.Helper.ConsoleTable; 3 | using adoAdmin.ViewModels; 4 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi; 5 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.Models; 6 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi; 7 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; 8 | using Microsoft.VisualStudio.Services.Common; 9 | using Microsoft.VisualStudio.Services.WebApi; 10 | using System; 11 | using System.Collections.Generic; 12 | 13 | namespace adoAdmin.Repos 14 | { 15 | public static class Fields 16 | { 17 | private readonly static string[] _fieldTypes = new string[] { "boolean", "dateTime", "double", "html", "identity", "integer", "pickactionDouble", "pickactionInteger", "pickactionString", "plainText", "string" }; 18 | 19 | public static List GetAllFields(VssConnection connection) 20 | { 21 | return GetAllFields(connection, false); 22 | } 23 | 24 | public static List GetAllFields(VssConnection connection, bool ignoreSystem) 25 | { 26 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 27 | 28 | //List fields = workItemTrackingClient.GetFieldsAsync().Result; 29 | List list = workItemTrackingClient.GetWorkItemFieldsAsync().Result; 30 | 31 | if (ignoreSystem) 32 | { 33 | list.RemoveAll(x => x.ReferenceName.Contains("System.") || x.ReferenceName.Contains("Microsoft.")); 34 | } 35 | 36 | return list; 37 | } 38 | 39 | public static List SearchFields(VssConnection connection, string name = null, string type = null) 40 | { 41 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 42 | 43 | List fields = workItemTrackingClient.GetFieldsAsync().Result; 44 | List results = new List(); 45 | 46 | if (! string.IsNullOrEmpty(name)) 47 | { 48 | foreach(var field in fields) 49 | { 50 | if (field.Name.ToLower().Contains(name.ToLower())) 51 | { 52 | results.Add(field); 53 | } 54 | } 55 | } 56 | 57 | if (! string.IsNullOrEmpty(type)) 58 | { 59 | var fieldType = SetFieldType(type); 60 | 61 | foreach (var field in fields) 62 | { 63 | if (field.Type == fieldType) 64 | { 65 | results.Add(field); 66 | } 67 | } 68 | } 69 | 70 | return results; 71 | } 72 | 73 | public static WorkItemField GetField(VssConnection connection, string refname) 74 | { 75 | WorkItemTrackingHttpClient client = connection.GetClient(); 76 | 77 | try 78 | { 79 | WorkItemField field = client.GetFieldAsync(refname).Result; 80 | 81 | return field; 82 | } 83 | catch (Exception) 84 | { 85 | return null; 86 | } 87 | } 88 | 89 | public static WorkItemTypeFieldWithReferences GetFieldForWorkItemType(VssConnection connection,string project, string workItemType, string fieldRefName) 90 | { 91 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 92 | 93 | WorkItemTypeFieldWithReferences field = workItemTrackingClient.GetWorkItemTypeFieldWithReferencesAsync(project, workItemType, fieldRefName).Result; 94 | 95 | return field; 96 | } 97 | 98 | public static WorkItemField AddField(VssConnection connection, string refname, string name, string type) 99 | { 100 | WorkItemTrackingHttpClient client = connection.GetClient(); 101 | Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType fieldType = SetFieldType(type); 102 | 103 | WorkItemField workItemField = new WorkItemField() 104 | { 105 | Name = name, 106 | ReferenceName = refname, 107 | Type = fieldType 108 | }; 109 | 110 | var result = client.CreateFieldAsync(workItemField).Result; 111 | 112 | return result; 113 | } 114 | 115 | public static List ListFieldsForProcess(VssConnection connection, string process) 116 | { 117 | WorkItemTrackingProcessHttpClient client = connection.GetClient(); 118 | List list = new List(); 119 | 120 | //get the process by name 121 | List listProcess = client.GetListOfProcessesAsync().Result; 122 | 123 | ProcessInfo processInfo = listProcess.Find(x => x.Name == process); 124 | 125 | //if processInfo is null then just return null 126 | if (processInfo == null) 127 | { 128 | return null; 129 | } 130 | 131 | //get list of work item types per the processid 132 | List listWorkItemTypes = client.GetProcessWorkItemTypesAsync(processInfo.TypeId).Result; 133 | 134 | //loop thru each wit and get the list of fields 135 | //add to viewmodel object and return that 136 | foreach(ProcessWorkItemType wit in listWorkItemTypes) 137 | { 138 | List listFields = client.GetAllWorkItemTypeFieldsAsync(processInfo.TypeId, wit.ReferenceName).Result; 139 | 140 | if (listFields.Count > 0) 141 | { 142 | list.Add(new FieldsPerProcess() { workItemType = wit, fields = listFields }); 143 | } 144 | else 145 | { 146 | list.Add(null); 147 | } 148 | } 149 | 150 | return list; 151 | } 152 | 153 | public static Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType SetFieldType(string type) 154 | { 155 | Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType fieldType; 156 | 157 | switch (type) 158 | { 159 | case "string": 160 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.String; 161 | break; 162 | case "boolean": 163 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.Boolean; 164 | break; 165 | case "dateTime": 166 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.DateTime; 167 | break; 168 | case "double": 169 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.Double; 170 | break; 171 | case "html": 172 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.Html; 173 | break; 174 | case "identity": 175 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.Identity; 176 | break; 177 | case "integer": 178 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.Integer; 179 | break; 180 | case "plainText": 181 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.PlainText; 182 | break; 183 | case "pickactionDouble": 184 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.PicklistDouble; 185 | break; 186 | case "pickactionInteger": 187 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.PicklistInteger; 188 | break; 189 | case "pickactionString": 190 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.PicklistString; 191 | break; 192 | default: 193 | fieldType = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.FieldType.String; 194 | break; 195 | } 196 | 197 | return fieldType; 198 | } 199 | 200 | public static string[] Types 201 | { 202 | get { return _fieldTypes; } 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Repos/Process.cs: -------------------------------------------------------------------------------- 1 | using adoAdmin.Helper; 2 | using adoAdmin.Helper.ConsoleTable; 3 | using adoAdmin.ViewModels; 4 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi; 5 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.Models; 6 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi; 7 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; 8 | using Microsoft.VisualStudio.Services.ActivityStatistic; 9 | using Microsoft.VisualStudio.Services.Common; 10 | using Microsoft.VisualStudio.Services.WebApi; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Reflection; 14 | 15 | namespace adoAdmin.Repos 16 | { 17 | public static class Process 18 | { 19 | public static List GetProcesses(VssConnection connection) 20 | { 21 | return GetProcesses(connection, false); 22 | } 23 | 24 | public static List GetProcesses(VssConnection connection, bool ignoreSystem) 25 | { 26 | WorkItemTrackingProcessHttpClient client = connection.GetClient(); 27 | 28 | try 29 | { 30 | List list = client.GetListOfProcessesAsync().Result; 31 | 32 | if (ignoreSystem) 33 | { 34 | list.RemoveAll(x => x.Name == "CMMI" || x.Name == "Agile" || x.Name == "Scrum" || x.Name == "Basic"); 35 | } 36 | 37 | return list; 38 | } 39 | catch (Exception) 40 | { 41 | return null; 42 | } 43 | } 44 | 45 | public static List GetWorkItemTypes(VssConnection connection, System.Guid processId) 46 | { 47 | WorkItemTrackingProcessHttpClient client = connection.GetClient(); 48 | 49 | try 50 | { 51 | List list = client.GetProcessWorkItemTypesAsync(processId).Result; 52 | 53 | return list; 54 | } 55 | catch (Exception) 56 | { 57 | return null; 58 | } 59 | } 60 | 61 | public static ProcessWorkItemType GetWorkItemType(VssConnection connection, System.Guid processId, string witRefName) 62 | { 63 | WorkItemTrackingProcessHttpClient client = connection.GetClient(); 64 | 65 | try 66 | { 67 | ProcessWorkItemType result = client.GetProcessWorkItemTypeAsync(processId, witRefName).Result; 68 | 69 | return result; 70 | } 71 | catch (Exception) 72 | { 73 | return null; 74 | } 75 | } 76 | 77 | public static ProcessWorkItemTypeField GetField(VssConnection connection, Guid processId, string witRefName, string fieldRefName) 78 | { 79 | WorkItemTrackingProcessHttpClient client = connection.GetClient(); 80 | 81 | try 82 | { 83 | ProcessWorkItemTypeField item = client.GetWorkItemTypeFieldAsync(processId, witRefName, fieldRefName).Result; 84 | 85 | return item; 86 | } 87 | catch (Exception ex) 88 | { 89 | return null; 90 | } 91 | } 92 | 93 | public static ProcessWorkItemType CloneWorkItemType(VssConnection connection, string witRefName, Guid processId) 94 | { 95 | ProcessWorkItemType wit = Process.GetWorkItemType(connection, processId, witRefName); 96 | 97 | if (wit == null) return null; 98 | 99 | WorkItemTrackingProcessHttpClient client = connection.GetClient(); 100 | 101 | CreateProcessWorkItemTypeRequest createWitRequest = new CreateProcessWorkItemTypeRequest() 102 | { 103 | Color = wit.Color, 104 | Description = wit.Description, 105 | Name = wit.Name, 106 | Icon = wit.Icon, 107 | InheritsFrom = wit.Inherits, 108 | IsDisabled = false 109 | }; 110 | 111 | ProcessWorkItemType results = client.CreateProcessWorkItemTypeAsync(createWitRequest, processId).Result; 112 | 113 | return results; 114 | } 115 | 116 | public static List ListPicklists(VssConnection connection) 117 | { 118 | WorkItemTrackingProcessHttpClient client = connection.GetClient(); 119 | 120 | try 121 | { 122 | List list = client.GetListsMetadataAsync(connection).Result; 123 | 124 | return list; 125 | } 126 | catch (Exception) 127 | { 128 | return null; 129 | } 130 | } 131 | 132 | public static Boolean DeletePicklist(VssConnection connection, string id) 133 | { 134 | WorkItemTrackingProcessHttpClient client = connection.GetClient(); 135 | 136 | try 137 | { 138 | Guid listId = new Guid(id); 139 | 140 | client.DeleteListAsync(listId).SyncResult(); 141 | return true; 142 | } 143 | catch (Exception) 144 | { 145 | return false; 146 | } 147 | } 148 | 149 | 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Repos/Projects.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.TeamFoundation.Core.WebApi; 2 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi; 3 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; 4 | using Microsoft.VisualStudio.Services.WebApi; 5 | 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace adoAdmin.Repos 10 | { 11 | public static class Projects 12 | { 13 | public static List GetAllProjects(VssConnection connection) 14 | { 15 | ProjectHttpClient client = connection.GetClient(); 16 | 17 | IPagedList projects = client.GetProjects(ProjectState.WellFormed).Result; 18 | 19 | return projects.ToList(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Repos/RecycleBin.cs: -------------------------------------------------------------------------------- 1 | using adoAdmin.Helper; 2 | using adoAdmin.Helper.ConsoleTable; 3 | using adoAdmin.ViewModels; 4 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi; 5 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.Models; 6 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi; 7 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; 8 | using Microsoft.VisualStudio.Services.ActivityStatistic; 9 | using Microsoft.VisualStudio.Services.Common; 10 | using Microsoft.VisualStudio.Services.WebApi; 11 | using Newtonsoft.Json; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Net.Http.Headers; 15 | using System.Net.Http; 16 | using System.Text; 17 | using System.Linq; 18 | using Microsoft.VisualStudio.Services.Organization.Client; 19 | using adoAdmin.Http; 20 | 21 | namespace adoAdmin.Repos 22 | { 23 | public static class RecycleBin 24 | { 25 | public static List GetDeletedWorkItems(VssConnection connection, string project) 26 | { 27 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 28 | 29 | List wits = workItemTrackingClient.GetDeletedWorkItemShallowReferencesAsync(project).Result; 30 | 31 | return wits; 32 | } 33 | 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// Name or project 39 | /// Number of days to ignore from today. For example, use 365 if you don't want to get the work items that have been deleted in the last year. Use 0 if you want to get everything. 40 | /// 41 | public static List GetDeletedWorkItemsByWiql(VssConnection connection, string project, int days = 0) 42 | { 43 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 44 | 45 | // if we don't use days, then set wiql accordingly 46 | string wiqlString = days == 0 47 | ? $"Select [System.Id] From WorkItems Where [System.TeamProject] = '{project}' AND [System.IsDeleted] = true AND ([System.WorkItemType] NOT IN GROUP 'Microsoft.TestPlanCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.TestSuiteCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.TestCaseCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.SharedParameterCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.SharedStepCategory') ORDER BY [System.ChangedDate] ASC" 48 | : $"Select [System.Id] From WorkItems Where [System.TeamProject] = '{project}' AND [System.IsDeleted] = true AND [System.ChangedDate] < @Today - {days} AND ([System.WorkItemType] NOT IN GROUP 'Microsoft.TestPlanCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.TestSuiteCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.TestCaseCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.SharedParameterCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.SharedStepCategory') ORDER BY [System.ChangedDate] ASC"; 49 | 50 | Wiql wiql = new Wiql() { Query = wiqlString }; 51 | WorkItemQueryResult wits = workItemTrackingClient.QueryByWiqlAsync(wiql, false, 200).Result; 52 | 53 | return wits.WorkItems.ToList(); 54 | } 55 | 56 | public static IDestroyWorkItemsResponse DestroyWorkItems(string pat, string organizationUrl, string ids) { 57 | 58 | IDestroyWorkItemsResponse returnResponse = new DestroyWorkItemsResponse(); 59 | 60 | using (var client = new HttpClient()) 61 | { 62 | //client.BaseAddress = new Uri(org); 63 | client.DefaultRequestHeaders.Accept.Clear(); 64 | client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); 65 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", 66 | Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}", "", pat)))); 67 | 68 | string payload = "{\"Ids\": [" + ids + "],\"destroy\": true, \"skipNotifications\": true}"; 69 | HttpContent body = new StringContent(payload, Encoding.UTF8, "application/json"); 70 | HttpResponseMessage response = client.PostAsync($"{organizationUrl}/_apis/wit/workitemsdelete?api-version=7.1-preview.1", body).Result; 71 | 72 | returnResponse.StatusCode = response.StatusCode; 73 | returnResponse.Message = response.Content.ReadAsStringAsync().Result.ToString(); 74 | returnResponse.Success = response.StatusCode == System.Net.HttpStatusCode.NoContent ? true : false; 75 | 76 | client.Dispose(); 77 | 78 | return returnResponse; 79 | } 80 | } 81 | 82 | public static void DestroyWorkItem(VssConnection connection, int id) 83 | { 84 | WorkItemTrackingHttpClient client = connection.GetClient(); 85 | client.DestroyWorkItemAsync(id).SyncResult(); 86 | } 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Repos/Tags.cs: -------------------------------------------------------------------------------- 1 | using adoAdmin.Helper; 2 | using adoAdmin.Helper.ConsoleTable; 3 | using adoAdmin.ViewModels; 4 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi; 5 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.Models; 6 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi; 7 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; 8 | using Microsoft.VisualStudio.Services.ActivityStatistic; 9 | using Microsoft.VisualStudio.Services.Common; 10 | using Microsoft.VisualStudio.Services.WebApi; 11 | using Newtonsoft.Json; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Net.Http.Headers; 15 | using System.Net.Http; 16 | using System.Text; 17 | using System.Linq; 18 | using Microsoft.VisualStudio.Services.Organization.Client; 19 | using System.IO; 20 | using System.Xml.Linq; 21 | 22 | namespace adoAdmin.Repos 23 | { 24 | public static class Tags 25 | { 26 | public static List GetAllTags(VssConnection connection, string project) 27 | { 28 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 29 | 30 | List tags = workItemTrackingClient.GetTagsAsync(project).Result; 31 | 32 | return tags; 33 | } 34 | 35 | public static List FetchWorkItemByTag(VssConnection connection, string project, string tag) 36 | { 37 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 38 | 39 | // if we don't use days, then set wiql accordingly 40 | string wiqlString = $"Select [System.Id] From WorkItems Where [System.TeamProject] = '{project}' AND [System.Tags] CONTAINS '{tag}'"; 41 | 42 | Wiql wiql = new Wiql() { Query = wiqlString }; 43 | WorkItemQueryResult wits = workItemTrackingClient.QueryByWiqlAsync(wiql, false, 200).Result; 44 | 45 | return wits.WorkItems.ToList(); 46 | } 47 | 48 | public static void DeleteTag(VssConnection connection, string project, string name) 49 | { 50 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 51 | List tags = workItemTrackingClient.GetTagsAsync(project).Result; 52 | WorkItemTagDefinition specificTag = tags.FirstOrDefault(tag => tag.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); 53 | 54 | //if the tag exists delete it 55 | if (specificTag != null) 56 | { 57 | workItemTrackingClient.DeleteTagAsync(project, specificTag.Id.ToString()).SyncResult(); 58 | } 59 | //If the tag doesn't exist throw an error we can catch to make this case work the same as a TF error or other lower level issues. 60 | else 61 | { 62 | throw new Exception("TagNotFound: The following tag does not exist: " + specificTag.Name + ". Verify that the name of the project is correct and that the tag exists on the specified project."); 63 | } 64 | 65 | } 66 | 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /Repos/Teams.cs: -------------------------------------------------------------------------------- 1 | using adoAdmin.Helper; 2 | using adoAdmin.Helper.ConsoleTable; 3 | using adoAdmin.ViewModels; 4 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi; 5 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.Models; 6 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi; 7 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; 8 | using Microsoft.VisualStudio.Services.ActivityStatistic; 9 | using Microsoft.VisualStudio.Services.Common; 10 | using Microsoft.VisualStudio.Services.WebApi; 11 | using Newtonsoft.Json; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Net.Http.Headers; 15 | using System.Net.Http; 16 | using System.Text; 17 | using System.Linq; 18 | using Microsoft.VisualStudio.Services.Organization.Client; 19 | 20 | namespace adoAdmin.Repos 21 | { 22 | public static class Teams 23 | { 24 | public static List ListAll(VssConnection connection, string project) 25 | { 26 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 27 | 28 | List wits = workItemTrackingClient.GetDeletedWorkItemShallowReferencesAsync(project).Result; 29 | 30 | return wits; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Repos/WorkItemTypes.cs: -------------------------------------------------------------------------------- 1 | using adoAdmin.Helper; 2 | using adoAdmin.Helper.ConsoleTable; 3 | using adoAdmin.ViewModels; 4 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi; 5 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.Models; 6 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi; 7 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; 8 | using Microsoft.VisualStudio.Services.Common; 9 | using Microsoft.VisualStudio.Services.WebApi; 10 | using System; 11 | using System.Collections.Generic; 12 | 13 | namespace adoAdmin.Repos 14 | { 15 | public static class WorkItemTypes 16 | { 17 | public static List GetWorkItemTypesForProject(VssConnection connection, string project) 18 | { 19 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 20 | 21 | List wits = workItemTrackingClient.GetWorkItemTypesAsync(project).Result; 22 | 23 | return wits; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Repos/WorkItems.cs: -------------------------------------------------------------------------------- 1 | using adoAdmin.Helper; 2 | using adoAdmin.Helper.ConsoleTable; 3 | using adoAdmin.ViewModels; 4 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi; 5 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.Models; 6 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi; 7 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; 8 | using Microsoft.VisualStudio.Services.ActivityStatistic; 9 | using Microsoft.VisualStudio.Services.Common; 10 | using Microsoft.VisualStudio.Services.WebApi; 11 | using Newtonsoft.Json; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Net.Http.Headers; 15 | using System.Net.Http; 16 | using System.Text; 17 | using System.Linq; 18 | using Microsoft.VisualStudio.Services.Organization.Client; 19 | 20 | namespace adoAdmin.Repos 21 | { 22 | public static class WorkItems 23 | { 24 | public static List GetDeletedWorkItems(VssConnection connection, string project) 25 | { 26 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 27 | 28 | List wits = workItemTrackingClient.GetDeletedWorkItemShallowReferencesAsync(project).Result; 29 | 30 | return wits; 31 | } 32 | 33 | public static List GetWorkItemsByWiql(VssConnection connection, string project) 34 | { 35 | WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient(); 36 | 37 | // if we don't use days, then set wiql accordingly 38 | string wiqlString = 39 | $"Select [System.Id] From WorkItems Where [System.TeamProject] = '{project}' AND ([System.WorkItemType] NOT IN GROUP 'Microsoft.TestPlanCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.TestSuiteCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.TestCaseCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.SharedParameterCategory' AND [System.WorkItemType] NOT IN GROUP 'Microsoft.SharedStepCategory') ORDER BY [System.ChangedDate] ASC"; 40 | 41 | Wiql wiql = new Wiql() { Query = wiqlString }; 42 | WorkItemQueryResult wits = workItemTrackingClient.QueryByWiqlAsync(wiql, false, top: 19999).Result; 43 | 44 | return wits.WorkItems.ToList(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ViewModels/FieldsPerProcess.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace adoAdmin.ViewModels 9 | { 10 | public class FieldsPerProcess 11 | { 12 | public ProcessWorkItemType workItemType { get; set;} 13 | public List fields { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ViewModels/PlanList.cs: -------------------------------------------------------------------------------- 1 | using adoAdmin.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net; 5 | 6 | namespace adoAdmin.ViewModels 7 | { 8 | public class PlanList 9 | { 10 | public int count { get; set; } 11 | public List value { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ado-admin.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {38B89549-EC6D-432E-B3DC-F7FBDB9715FD} 8 | Exe 9 | adoAdmin 10 | adoadmin 11 | v4.8 12 | 512 13 | true 14 | true 15 | 16 | 17 | 18 | 19 | 20 | AnyCPU 21 | true 22 | full 23 | false 24 | bin\Debug\ 25 | DEBUG;TRACE 26 | prompt 27 | 4 28 | 29 | 30 | AnyCPU 31 | pdbonly 32 | true 33 | bin\Release\ 34 | TRACE 35 | prompt 36 | 4 37 | 38 | 39 | 40 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.Azure.DevOps.Comments.WebApi.dll 41 | 42 | 43 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.Azure.Pipelines.WebApi.dll 44 | 45 | 46 | packages\Microsoft.Bcl.AsyncInterfaces.9.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll 47 | 48 | 49 | packages\Microsoft.Bcl.TimeProvider.9.0.0\lib\net462\Microsoft.Bcl.TimeProvider.dll 50 | 51 | 52 | packages\Microsoft.IdentityModel.Abstractions.8.3.0\lib\net472\Microsoft.IdentityModel.Abstractions.dll 53 | 54 | 55 | packages\Microsoft.IdentityModel.JsonWebTokens.8.3.0\lib\net472\Microsoft.IdentityModel.JsonWebTokens.dll 56 | 57 | 58 | packages\Microsoft.IdentityModel.Logging.8.3.0\lib\net472\Microsoft.IdentityModel.Logging.dll 59 | 60 | 61 | packages\Microsoft.IdentityModel.Tokens.8.3.0\lib\net472\Microsoft.IdentityModel.Tokens.dll 62 | 63 | 64 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.Build2.WebApi.dll 65 | 66 | 67 | packages\Microsoft.VisualStudio.Services.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.Common.dll 68 | 69 | 70 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.Core.WebApi.dll 71 | 72 | 73 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.Dashboards.WebApi.dll 74 | 75 | 76 | packages\Microsoft.TeamFoundation.DistributedTask.Common.Contracts.19.225.1\lib\net472\Microsoft.TeamFoundation.DistributedTask.Common.Contracts.dll 77 | 78 | 79 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.Pipelines.WebApi.dll 80 | 81 | 82 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.Policy.WebApi.dll 83 | 84 | 85 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.SourceControl.WebApi.dll 86 | 87 | 88 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.Test.WebApi.dll 89 | 90 | 91 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.TestManagement.WebApi.dll 92 | 93 | 94 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.Wiki.WebApi.dll 95 | 96 | 97 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.Work.WebApi.dll 98 | 99 | 100 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.dll 101 | 102 | 103 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.TeamFoundation.WorkItemTracking.WebApi.dll 104 | 105 | 106 | packages\Microsoft.VisualStudio.Services.Client.19.225.1\lib\net472\Microsoft.VisualStudio.Services.Common.dll 107 | 108 | 109 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.VisualStudio.Services.TestManagement.TestPlanning.WebApi.dll 110 | 111 | 112 | packages\Microsoft.TeamFoundationServer.Client.19.225.1\lib\net472\Microsoft.VisualStudio.Services.TestResults.WebApi.dll 113 | 114 | 115 | packages\Microsoft.VisualStudio.Services.Client.19.225.1\lib\net472\Microsoft.VisualStudio.Services.WebApi.dll 116 | 117 | 118 | packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll 119 | 120 | 121 | packages\Newtonsoft.Json.Bson.1.0.3\lib\net45\Newtonsoft.Json.Bson.dll 122 | 123 | 124 | 125 | packages\System.Buffers.4.6.0\lib\net462\System.Buffers.dll 126 | 127 | 128 | 129 | packages\System.IdentityModel.Tokens.Jwt.8.3.0\lib\net472\System.IdentityModel.Tokens.Jwt.dll 130 | 131 | 132 | packages\System.IO.Pipelines.9.0.0\lib\net462\System.IO.Pipelines.dll 133 | 134 | 135 | packages\System.Memory.4.6.0\lib\net462\System.Memory.dll 136 | 137 | 138 | packages\Microsoft.AspNet.WebApi.Client.6.0.0\lib\net45\System.Net.Http.Formatting.dll 139 | 140 | 141 | 142 | packages\System.Numerics.Vectors.4.6.0\lib\net462\System.Numerics.Vectors.dll 143 | 144 | 145 | packages\System.Runtime.CompilerServices.Unsafe.6.1.0\lib\net462\System.Runtime.CompilerServices.Unsafe.dll 146 | 147 | 148 | 149 | packages\System.Text.Encodings.Web.9.0.0\lib\net462\System.Text.Encodings.Web.dll 150 | 151 | 152 | packages\System.Text.Json.9.0.0\lib\net462\System.Text.Json.dll 153 | 154 | 155 | packages\System.Threading.Tasks.Extensions.4.6.0\lib\net462\System.Threading.Tasks.Extensions.dll 156 | 157 | 158 | packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll 159 | 160 | 161 | 162 | packages\Microsoft.AspNet.WebApi.Core.5.3.0\lib\net45\System.Web.Http.dll 163 | 164 | 165 | packages\Microsoft.AspNet.WebApi.WebHost.5.3.0\lib\net45\System.Web.Http.WebHost.dll 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | Always 199 | 200 | 201 | Always 202 | 203 | 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /ado-admin.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33027.239 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ado-admin", "ado-admin.csproj", "{38B89549-EC6D-432E-B3DC-F7FBDB9715FD}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {38B89549-EC6D-432E-B3DC-F7FBDB9715FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {38B89549-EC6D-432E-B3DC-F7FBDB9715FD}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {38B89549-EC6D-432E-B3DC-F7FBDB9715FD}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {38B89549-EC6D-432E-B3DC-F7FBDB9715FD}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {3EBB601D-19CD-40B8-83CC-83BA1E6C4332} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization": "", 3 | "personalaccesstoken": "" 4 | } -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | --------------------------------------------------------------------------------