├── .gitattributes ├── .gitignore ├── README.md ├── Tfs.BuildNotifications ├── Tfs.BuildNotifications.Common │ ├── Encryption │ │ └── EncryptionService.cs │ ├── Extensions │ │ └── StringExtensions.cs │ ├── Helpers │ │ ├── Interfaces │ │ │ └── IRegistryHelper.cs │ │ └── RegistryHelper.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Telemetry │ │ ├── Interfaces │ │ │ └── ILogService.cs │ │ └── LogService.cs │ └── Tfs.BuildNotifications.Common.csproj ├── Tfs.BuildNotifications.Core │ ├── Clients │ │ ├── DTOs │ │ │ ├── BuildApiResponses.cs │ │ │ └── ProjectListApiResponse.cs │ │ ├── Interfaces │ │ │ └── ITfsApiClient.cs │ │ └── TfsApiClient.cs │ ├── Exceptions │ │ └── ServiceValidationException.cs │ ├── Extensions │ │ └── BuildExtensions.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Services │ │ ├── BuildConfigurationService.cs │ │ ├── Interfaces │ │ │ ├── IBuildConfigurationService.cs │ │ │ └── IPollingService.cs │ │ └── PollingService.cs │ ├── Tfs.BuildNotifications.Core.csproj │ └── packages.config ├── Tfs.BuildNotifications.Model │ ├── Build.cs │ ├── BuildConfig.cs │ ├── BuildStatus.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Tfs.BuildNotifications.Model.csproj │ └── TfsServerDeployment.cs ├── Tfs.BuildNotifications.Setup │ ├── Product.wxs │ ├── Tfs.BuildNotifications.Setup.wixproj │ ├── banner.bmp │ ├── dialog.bmp │ ├── license.rtf │ └── variables.wxi ├── Tfs.BuildNotifications.Tray │ ├── App.config │ ├── Infrastructure │ │ ├── Config │ │ │ ├── AppConfig.cs │ │ │ └── Interfaces │ │ │ │ └── IAppConfig.cs │ │ └── Unity │ │ │ └── Bootstrapper.cs │ ├── Lib │ │ ├── Windows.Foundation.FoundationContract.winmd │ │ └── Windows.Foundation.UniversalApiContract.winmd │ ├── Program.cs │ ├── Properties │ │ ├── AssemblyInfo.cs │ │ ├── Resources.Designer.cs │ │ ├── Resources.resx │ │ ├── Settings.Designer.cs │ │ └── Settings.settings │ ├── Resources │ │ ├── tfs-icon-build-fail.ico │ │ └── tfs-icon.ico │ ├── Services │ │ ├── Interfaces │ │ │ └── INotificationService.cs │ │ ├── NotificationService.cs │ │ ├── TextToSpeechNotificationService.cs │ │ ├── ToastNotificationService.cs │ │ └── ToolTipNotificationService.cs │ ├── Tfs.BuildNotifications.Tray.csproj │ ├── TrayIconApplicationContext.cs │ ├── app.manifest │ └── packages.config ├── Tfs.BuildNotifications.Web │ ├── Content │ │ ├── Images │ │ │ ├── close.png │ │ │ ├── delete.png │ │ │ ├── failed.png │ │ │ ├── minus.png │ │ │ ├── play.png │ │ │ ├── plus.png │ │ │ ├── stopped.png │ │ │ ├── success.png │ │ │ ├── tfs-logo.png │ │ │ └── unknown.png │ │ ├── Scripts │ │ │ ├── dashboard.js │ │ │ └── jquery.signalR-2.2.1.js │ │ ├── Styles │ │ │ └── site.css │ │ └── favicon.ico │ ├── Nancy │ │ ├── Configuration │ │ │ ├── NancyCustomBootstrapper.cs │ │ │ └── NancyRazorConfiguration.cs │ │ ├── Handlers │ │ │ ├── PageNotFoundHandler.cs │ │ │ └── ServerErrorHandler.cs │ │ ├── Modules │ │ │ ├── ConnectionModule.cs │ │ │ └── HomeModule.cs │ │ └── Validators │ │ │ └── AddConnectionValidator.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Scripts │ │ ├── jquery-1.6.4-vsdoc.js │ │ ├── jquery-1.6.4.js │ │ ├── jquery-1.6.4.min.js │ │ ├── jquery.signalR-2.2.2.js │ │ └── jquery.signalR-2.2.2.min.js │ ├── Services │ │ ├── Interfaces │ │ │ └── IWebsiteDashboardService.cs │ │ └── WebsiteDashboardService.cs │ ├── SignalR │ │ ├── DashboardHub.cs │ │ └── Interfaces │ │ │ └── IDashboardHub.cs │ ├── Startup.cs │ ├── Tfs.BuildNotifications.Web.csproj │ ├── ViewModels │ │ ├── AddBuildsViewModel.cs │ │ ├── AddEditConnectionViewModel.cs │ │ ├── AddProjectViewModel.cs │ │ ├── HomeViewModel.cs │ │ ├── SingleBuildDefinitionViewModel.cs │ │ └── ViewModelBase.cs │ ├── Views │ │ ├── AddBuilds.cshtml │ │ ├── AddConnection.cshtml │ │ ├── AddProject.cshtml │ │ ├── Configure.cshtml │ │ ├── EditConnection.cshtml │ │ ├── Error.cshtml │ │ ├── Index.cshtml │ │ ├── NotFound.cshtml │ │ └── Shared │ │ │ ├── _AddEditConnection.cshtml │ │ │ ├── _BuildSummary.cshtml │ │ │ └── _Layout.cshtml │ ├── app.config │ └── packages.config └── Tfs.BuildNotifications.sln ├── docs └── images │ ├── build-failed.png │ ├── build-passed.png │ ├── build-started.png │ └── dashboard-example.png └── downloads └── tfs-build-notifications-setup.msi /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TFS Build Notifications 2 | A notification and alert system for Microsoft Team Foundation Server builds. 3 | 4 | ## Overview 5 | 6 | A Windows Forms application which provides notifications for TFS builds. Includes a website dashboard that allows you track the status of build definitions across multiple TFS connections and projects. 7 | 8 | ### Download 9 | 10 | Download the installer for the current version (v1.0) [here](https://github.com/mattwendels/tfs-build-notifications/raw/master/downloads/tfs-build-notifications-setup.msi). 11 | 12 | ### Dashboard 13 | 14 | The website dashboard gives a detailed overview of your monitored build definitions across multiple connections (via on-premises TFS installations and/or Visual Studio Online). Real time updates are applied to the dashboard when the status of a monitored build changes. 15 | 16 | A recent build history is displayed underneath each build definition. 17 | 18 | ![Website dashboard](/docs/images/dashboard-example.png) 19 | 20 | ### Notifications 21 | 22 | Build changes are polled at a configurable interval and display tray notifications (or Toast notifications in Windows 10) when a build starts, stops, fails or succeeds. If required, the application can be also configured to only display a notification if a build fails. 23 | 24 | ![Build started](/docs/images/build-started.png) 25 | 26 | ![Build failed](/docs/images/build-failed.png) 27 | 28 | ![Build succeeded](/docs/images/build-passed.png) 29 | 30 | ### Notification TTS (text to speech) 31 | 32 | In addition to existing notifications you can activate voice output by setting TextToSpeech.Enabled to true in the app.config file. 33 | 34 | ### Application 35 | 36 | .Net 4.5 Windows Forms application using Owin, Nancy and SignalR to self host and run the website dashboard. 37 | 38 | #### Installer 39 | 40 | Download the installer for the current version (v1.0) [here](https://github.com/mattwendels/tfs-build-notifications/raw/master/downloads/tfs-build-notifications-setup.msi). 41 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Common/Encryption/EncryptionService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | 6 | namespace Tfs.BuildNotifications.Common.Encryption 7 | { 8 | public static class EncryptionService 9 | { 10 | /* AES encryption using a key and random initialisation vector for the most secure method. This is a modified version 11 | based on the examples here: http://stackoverflow.com/questions/8041451/good-aes-initialization-vector-practice */ 12 | 13 | /// 14 | /// Encrypts a string using AES encryption via the key provided. 15 | /// 16 | /// A key to encrpyt/decrypt the data with. 17 | /// The string data to encrpyt. 18 | /// A Base64 AES encrypted string. 19 | public static string AesEncryptString(string key, string toEncrypt) 20 | { 21 | var toEncryptBytes = Encoding.UTF8.GetBytes(toEncrypt); 22 | var keyBytes = Convert.FromBase64String(key); 23 | 24 | using (var aesProvider = new AesCryptoServiceProvider()) 25 | { 26 | aesProvider.Key = keyBytes; 27 | aesProvider.Mode = CipherMode.CBC; 28 | aesProvider.Padding = PaddingMode.PKCS7; 29 | 30 | aesProvider.GenerateIV(); 31 | 32 | using (var encryptor = aesProvider.CreateEncryptor(aesProvider.Key, aesProvider.IV)) 33 | { 34 | using (var memoryStream = new MemoryStream()) 35 | { 36 | //Prepend random, IV to the encrypted ciphertext. 37 | memoryStream.Write(aesProvider.IV, 0, 16); 38 | 39 | using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) 40 | { 41 | cryptoStream.Write(toEncryptBytes, 0, toEncryptBytes.Length); 42 | cryptoStream.FlushFinalBlock(); 43 | } 44 | 45 | return Convert.ToBase64String(memoryStream.ToArray()); 46 | } 47 | } 48 | } 49 | } 50 | 51 | /// 52 | /// Decrypts a string using AES decryption via the key provided. 53 | /// 54 | /// A key to decrypt the data with. 55 | /// The Base64 AES encrypted string data to decrypt. 56 | /// The decrypted UTF8 string. 57 | public static string AesDecryptString(string key, string encrypted) 58 | { 59 | var encryptedBytes = Convert.FromBase64String(encrypted); 60 | var keyBytes = Convert.FromBase64String(key); 61 | 62 | using (var aesProvider = new AesCryptoServiceProvider()) 63 | { 64 | aesProvider.Key = keyBytes; 65 | aesProvider.Mode = CipherMode.CBC; 66 | aesProvider.Padding = PaddingMode.PKCS7; 67 | 68 | using (var memoryStream = new MemoryStream(encryptedBytes)) 69 | { 70 | var orginalIv = new byte[16]; 71 | 72 | /* The IV should have been prepeneded in the original encryption, extract it here before decrypting 73 | the actual data. */ 74 | memoryStream.Read(orginalIv, 0, 16); 75 | 76 | aesProvider.IV = orginalIv; 77 | 78 | using (var decryptor = aesProvider.CreateDecryptor(aesProvider.Key, aesProvider.IV)) 79 | { 80 | using (var cs = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) 81 | { 82 | var decrypted = new byte[encryptedBytes.Length]; 83 | var byteCount = cs.Read(decrypted, 0, encryptedBytes.Length); 84 | 85 | return Encoding.UTF8.GetString(decrypted, 0, byteCount); 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Common/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Tfs.BuildNotifications.Common.Extensions 2 | { 3 | public static class StringExtensions 4 | { 5 | public static string Shorten(this string s, int length, string suffix = null) 6 | { 7 | if (string.IsNullOrWhiteSpace(s)) 8 | { 9 | return string.Empty; 10 | } 11 | 12 | if (s.Length <= length) 13 | { 14 | return s; 15 | } 16 | 17 | var trimmed = s.Substring(0, length); 18 | 19 | return !string.IsNullOrWhiteSpace(suffix) ? trimmed + suffix : trimmed; 20 | } 21 | 22 | public static string Pluralize(this string val, int count) 23 | { 24 | if (string.IsNullOrWhiteSpace(val)) 25 | { 26 | return string.Empty; 27 | } 28 | 29 | if (count == 1) 30 | { 31 | return val; 32 | } 33 | 34 | if (val.ToLower().EndsWith("s")) 35 | { 36 | return $"{val}'"; 37 | } 38 | 39 | return $"{val}s"; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Common/Helpers/Interfaces/IRegistryHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Tfs.BuildNotifications.Common.Helpers.Interfaces 2 | { 3 | public interface IRegistryHelper 4 | { 5 | T GetValue(string subKey, string name); 6 | 7 | void SetValue(string subKey, string name, string value); 8 | 9 | bool KeyExistsWithValue(string subKey, string name); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Common/Helpers/RegistryHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using System; 3 | using Tfs.BuildNotifications.Common.Helpers.Interfaces; 4 | 5 | namespace Tfs.BuildNotifications.Common.Helpers 6 | { 7 | public class RegistryHelper : IRegistryHelper 8 | { 9 | public T GetValue(string subKey, string name) 10 | { 11 | return (T)GetRegistryKey(subKey).GetValue(name); 12 | } 13 | 14 | public void SetValue(string subKey, string name, string value) 15 | { 16 | GetRegistryKey(subKey).SetValue(name, value); 17 | } 18 | 19 | public bool KeyExistsWithValue(string subKey, string name) 20 | { 21 | return !string.IsNullOrWhiteSpace(GetValue(subKey, name)); 22 | } 23 | 24 | #region Private Methods 25 | 26 | private RegistryKey GetRegistryKey(string subKey) 27 | { 28 | // Is the server a 64bit machine? Read registry keys correctly here... 29 | var baseRegistryArchitecture = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, 30 | (IntPtr.Size == 8 ? RegistryView.Registry64 : RegistryView.Registry32)); 31 | 32 | baseRegistryArchitecture.CreateSubKey(subKey); 33 | 34 | return baseRegistryArchitecture.OpenSubKey(subKey, true); 35 | } 36 | 37 | #endregion 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Common/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Tfs.BuildNotifications.Common")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("Tfs.BuildNotifications.Common")] 12 | [assembly: AssemblyCopyright("Copyright © 2017")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("f8ef6d0f-a56e-4ba6-b6ba-df9c4a5f6792")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Common/Telemetry/Interfaces/ILogService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Tfs.BuildNotifications.Common.Telemetry.Interfaces 4 | { 5 | public interface ILogService 6 | { 7 | void Log(string message, LogLevel logLevel = LogLevel.Info); 8 | void Log(Exception e, LogLevel logLevel = LogLevel.Error); 9 | void Log(string message, Exception e, LogLevel logLevel = LogLevel.Error); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Common/Telemetry/LogService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Tfs.BuildNotifications.Common.Telemetry.Interfaces; 4 | 5 | namespace Tfs.BuildNotifications.Common.Telemetry 6 | { 7 | public enum LogLevel 8 | { 9 | Info, 10 | Warning, 11 | Error 12 | } 13 | 14 | public class LogService : ILogService 15 | { 16 | private string _logFilePath = "tfs.buildnotifications.log"; 17 | 18 | private static object _logFileLock = new object(); 19 | 20 | public void Log(string message, LogLevel logLevel = LogLevel.Info) 21 | { 22 | LogToFile(message, logLevel); 23 | } 24 | 25 | public void Log(Exception e, LogLevel logLevel = LogLevel.Error) 26 | { 27 | LogToFile(e.ToString(), logLevel); 28 | } 29 | 30 | public void Log(string message, Exception e, LogLevel logLevel = LogLevel.Error) 31 | { 32 | LogToFile($"{message} - {e.ToString()}", logLevel); 33 | } 34 | 35 | private void LogToFile(string message, LogLevel logLevel) 36 | { 37 | try 38 | { 39 | lock(_logFileLock) 40 | { 41 | using (var writer = new StreamWriter(_logFilePath, true)) 42 | { 43 | writer.WriteLine($"{DateTime.Now} - {logLevel.ToString().ToUpper()} | {message}"); 44 | } 45 | } 46 | } 47 | catch { } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Common/Tfs.BuildNotifications.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {F8EF6D0F-A56E-4BA6-B6BA-DF9C4A5F6792} 8 | Library 9 | Properties 10 | Tfs.BuildNotifications.Common 11 | Tfs.BuildNotifications.Common 12 | v4.5.2 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/Clients/DTOs/BuildApiResponses.cs: -------------------------------------------------------------------------------- 1 | using RestSharp.Deserializers; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace Tfs.BuildNotifications.Core.Clients.DTOs 6 | { 7 | public class BuildDefinitionApiResponseWrapper 8 | { 9 | [DeserializeAs(Name = "value")] 10 | public List BuildDefinitions { get; set; } 11 | } 12 | 13 | public class BuildApiResponseWrapper 14 | { 15 | [DeserializeAs(Name = "value")] 16 | public List Builds { get; set; } 17 | } 18 | 19 | public class BuildApiResponse 20 | { 21 | [DeserializeAs(Name = "id")] 22 | public int BuildRunId { get; set; } 23 | 24 | [DeserializeAs(Name = "status")] 25 | public string Status { get; set; } 26 | 27 | [DeserializeAs(Name = "result")] 28 | public string Result { get; set; } 29 | 30 | [DeserializeAs(Name = "startTime")] 31 | public DateTime? StartTime { get; set; } 32 | 33 | [DeserializeAs(Name = "finishTime")] 34 | public DateTime? FinishTime { get; set; } 35 | 36 | [DeserializeAs(Name = "url")] 37 | public string Url { get; set; } 38 | 39 | [DeserializeAs(Name = "definition")] 40 | public BuildDefinitionApiResponse Definition { get; set; } 41 | 42 | [DeserializeAs(Name = "requestedFor")] 43 | public BuildRequestedByApiReponse Requestor { get; set; } 44 | 45 | [DeserializeAs(Name = "_links")] 46 | public Links Links { get; set; } 47 | } 48 | 49 | public class BuildDefinitionApiResponse 50 | { 51 | [DeserializeAs(Name = "name")] 52 | public string Name { get; set; } 53 | 54 | [DeserializeAs(Name = "url")] 55 | public string Url { get; set; } 56 | 57 | [DeserializeAs(Name = "id")] 58 | public int Id { get; set; } 59 | 60 | [DeserializeAs(Name = "_links")] 61 | public Links Links { get; set; } 62 | } 63 | 64 | public class BuildRequestedByApiReponse 65 | { 66 | [DeserializeAs(Name = "displayName")] 67 | public string Name { get; set; } 68 | 69 | [DeserializeAs(Name = "imageUrl")] 70 | public string ProfileImageUrl { get; set; } 71 | } 72 | 73 | public class Links 74 | { 75 | [DeserializeAs(Name = "web")] 76 | public WebLink Web { get; set; } 77 | } 78 | 79 | public class WebLink 80 | { 81 | [DeserializeAs(Name = "href")] 82 | public string Url { get; set; } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/Clients/DTOs/ProjectListApiResponse.cs: -------------------------------------------------------------------------------- 1 | using RestSharp.Deserializers; 2 | using System.Collections.Generic; 3 | using Tfs.BuildNotifications.Model; 4 | 5 | namespace Tfs.BuildNotifications.Core.Clients.DTOs 6 | { 7 | public class ProjectListApiResponse 8 | { 9 | [DeserializeAs(Name = "value")] 10 | public List Projects { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/Clients/Interfaces/ITfsApiClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Tfs.BuildNotifications.Model; 3 | 4 | namespace Tfs.BuildNotifications.Core.Clients.Interfaces 5 | { 6 | public interface ITfsApiClient 7 | { 8 | void TestConnection(string tfsServerUrl, TfsServerDeployment deployment, string userName, string password, 9 | string personalAccessToken); 10 | 11 | void TestConnection(Connection connection); 12 | 13 | IEnumerable GetProjects(Connection connection); 14 | 15 | IEnumerable GetBuildDefinitions(Connection connection, string projectName); 16 | 17 | List GetBuilds(Connection connection, string projectName, int buildDefinitionId); 18 | 19 | List GetLastBuildPerDefinition(Connection connection, string projectName, List buildDefinitionIds); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/Clients/TfsApiClient.cs: -------------------------------------------------------------------------------- 1 | using RestSharp; 2 | using RestSharp.Authenticators; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net; 6 | using Tfs.BuildNotifications.Common.Telemetry; 7 | using Tfs.BuildNotifications.Common.Telemetry.Interfaces; 8 | using Tfs.BuildNotifications.Core.Clients.DTOs; 9 | using Tfs.BuildNotifications.Core.Clients.Interfaces; 10 | using Tfs.BuildNotifications.Core.Exceptions; 11 | using Tfs.BuildNotifications.Model; 12 | 13 | namespace Tfs.BuildNotifications.Core.Clients 14 | { 15 | public class TfsApiClient : ITfsApiClient 16 | { 17 | private const string _buildsApiVersion = "2.0"; 18 | private const string _defaultApiVersion = "1.0"; 19 | 20 | private readonly ILogService _logService; 21 | 22 | public TfsApiClient(ILogService logService) 23 | { 24 | _logService = logService; 25 | } 26 | 27 | public void TestConnection(string tfsServerUrl, TfsServerDeployment deployment, string userName, string password, 28 | string personalAccessToken) 29 | { 30 | var connection = new Connection 31 | { 32 | TfsServerDeployment = deployment, 33 | UserName = userName, 34 | Password = password, 35 | PersonalAccessToken = personalAccessToken, 36 | TfsServerUrl = tfsServerUrl 37 | }; 38 | 39 | TestConnection(connection); 40 | } 41 | 42 | public void TestConnection(Connection connection) 43 | { 44 | this.DoApiRequest>(connection, $"_apis/projects?api-version={_defaultApiVersion}"); 45 | } 46 | 47 | public IEnumerable GetProjects(Connection connection) 48 | { 49 | return this.DoApiRequest(connection, $"_apis/projects?api-version={_defaultApiVersion}").Projects; 50 | } 51 | 52 | public IEnumerable GetBuildDefinitions(Connection connection, string projectName) 53 | { 54 | var result = this.DoApiRequest(connection, $"{projectName}/_apis/build/definitions?api-version={_buildsApiVersion}") 55 | .BuildDefinitions.OrderBy(b => b.Name); 56 | 57 | var buildDefinitions = new List(); 58 | 59 | foreach (var item in result) 60 | { 61 | // XAML builds have no links collection. These are ignored (and are marked for deprecation in TFS anyway). 62 | if (item.Links != null) 63 | { 64 | buildDefinitions.Add(new BuildDefinition 65 | { 66 | Id = item.Id, 67 | Name = item.Name, 68 | Url = item.Links.Web.Url 69 | }); 70 | } 71 | } 72 | 73 | return buildDefinitions; 74 | } 75 | 76 | public List GetBuilds(Connection connection, string projectName, int buildDefinitionId) 77 | { 78 | var apiBuildResult = this.DoApiRequest(connection, 79 | $"{projectName}/_apis/build/builds?api-version={_buildsApiVersion}&definitions={buildDefinitionId}&$top=10"); 80 | 81 | return ConvertToBuildModel(apiBuildResult); 82 | } 83 | 84 | public List GetLastBuildPerDefinition(Connection connection, string projectName, List buildDefinitionIds) 85 | { 86 | var apiBuildResult = this.DoApiRequest(connection, 87 | $"{projectName}/_apis/build/builds?api-version={_buildsApiVersion}&definitions={string.Join(",", buildDefinitionIds)}&maxBuildsPerDefinition=1"); 88 | 89 | return ConvertToBuildModel(apiBuildResult); 90 | } 91 | 92 | #region Private Methods 93 | 94 | private T DoApiRequest(Connection connection, string resource) where T : new() 95 | { 96 | var credentials = GetCredentials(connection); 97 | 98 | var client = new RestClient(connection.TfsServerUrl); 99 | 100 | if (connection.TfsServerDeployment == TfsServerDeployment.OnlineVsts) 101 | { 102 | client.Authenticator = new HttpBasicAuthenticator(credentials.UserName, credentials.Password); 103 | } 104 | else 105 | { 106 | client.Authenticator = new NtlmAuthenticator(credentials.UserName, credentials.Password); 107 | } 108 | 109 | var request = new RestRequest(resource); 110 | 111 | var response = client.Execute(request); 112 | 113 | var content = response.Content; 114 | 115 | if (response.StatusCode != HttpStatusCode.OK) 116 | { 117 | _logService.Log($"Failed to connect to TFS server {connection.TfsServerUrl}. Response: {response.StatusCode}", 118 | LogLevel.Error); 119 | 120 | throw new ServiceValidationException( 121 | $"Unable to connect to the specified TFS server. Server response: {response.StatusCode}"); 122 | } 123 | 124 | return response.Data; 125 | } 126 | 127 | private (string UserName, string Password) GetCredentials(Connection connection) 128 | { 129 | var basicAuthUserName = ""; 130 | var basicAuthPassword = ""; 131 | 132 | if (connection.TfsServerDeployment == TfsServerDeployment.OnPremises) 133 | { 134 | basicAuthUserName = connection.UserName; 135 | basicAuthPassword = connection.Password; 136 | } 137 | else if (connection.TfsServerDeployment == TfsServerDeployment.OnlineVsts) 138 | { 139 | basicAuthPassword = connection.PersonalAccessToken; 140 | } 141 | 142 | return (basicAuthUserName, basicAuthPassword); 143 | } 144 | 145 | private List ConvertToBuildModel(BuildApiResponseWrapper apiBuildResult) 146 | { 147 | var builds = new List(); 148 | 149 | foreach (var apiBuild in apiBuildResult.Builds) 150 | { 151 | builds.Add(new Build 152 | { 153 | DefinitionName = apiBuild.Definition.Name, 154 | LastFinished = apiBuild.FinishTime, 155 | LastRequestByProfileImageUrl = apiBuild.Requestor.ProfileImageUrl, 156 | LastRequestedBy = apiBuild.Requestor.Name, 157 | Result = apiBuild.Result, 158 | StartTime = apiBuild.StartTime, 159 | Status = apiBuild.Status, 160 | Url = apiBuild.Links.Web.Url, 161 | BuildRunId = apiBuild.BuildRunId 162 | }); 163 | } 164 | 165 | return builds; 166 | } 167 | 168 | #endregion 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/Exceptions/ServiceValidationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Tfs.BuildNotifications.Core.Exceptions 6 | { 7 | public class ServiceValidationException : Exception 8 | { 9 | public List ServiceErrors { get; set; } 10 | public Exception OriginalException { get; set; } 11 | 12 | public ServiceValidationException(string error, Exception originalException = null) 13 | : base(originalException != null ? originalException.Message : null) 14 | { 15 | ServiceErrors = new List { error }; 16 | OriginalException = originalException; 17 | } 18 | 19 | public ServiceValidationException(List errors) 20 | { 21 | ServiceErrors = errors; 22 | } 23 | 24 | public ServiceValidationException(Exception e) 25 | { 26 | OriginalException = e; 27 | } 28 | 29 | public override string Message => ToString(); 30 | 31 | public override string ToString() 32 | { 33 | var s = new StringBuilder("ServiceValidationException: "); 34 | 35 | foreach (var error in ServiceErrors) 36 | { 37 | s.Append($"Error: {error}. "); 38 | } 39 | 40 | s.Append($"Original exception: {OriginalException?.ToString() ?? ""}"); 41 | 42 | return s.ToString(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/Extensions/BuildExtensions.cs: -------------------------------------------------------------------------------- 1 | using Tfs.BuildNotifications.Model; 2 | 3 | namespace Tfs.BuildNotifications.Core.Extensions 4 | { 5 | public static class BuildExtensions 6 | { 7 | public static BuildResult GetBuildResult(this Build build) 8 | { 9 | var buildResult = Model.BuildResult.Unknown; 10 | 11 | switch (build?.Result?.Trim().ToLower()) 12 | { 13 | case "succeeded": 14 | buildResult = BuildResult.Succeeded; 15 | break; 16 | 17 | case "failed": 18 | buildResult = BuildResult.Failed; 19 | break; 20 | 21 | case "canceled": 22 | buildResult = BuildResult.Stopped; 23 | break; 24 | } 25 | 26 | if (buildResult == BuildResult.Unknown && build.InProgress) 27 | { 28 | buildResult = BuildResult.InProgress; 29 | } 30 | 31 | return buildResult; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Tfs.BuildNotifications.Core")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("Tfs.BuildNotifications.Core")] 12 | [assembly: AssemblyCopyright("Copyright © 2017")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("12d1f80b-4cda-460d-8ab4-5b0bda802f9e")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/Services/Interfaces/IBuildConfigurationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Tfs.BuildNotifications.Model; 4 | 5 | namespace Tfs.BuildNotifications.Core.Services.Interfaces 6 | { 7 | public interface IBuildConfigurationService 8 | { 9 | bool HasConfiguration(); 10 | 11 | bool HasAnyMonitoredBuilds(); 12 | 13 | BuildConfig GetBuildConfig(); 14 | 15 | void Init(); 16 | 17 | void AddConnection(string tfsServerUrl, string tfsLocation, string userName, string password, 18 | string personalAccessToken); 19 | 20 | void EditConnection(string connectionId, string tfsServerUrl, string tfsLocation, string userName, string password, 21 | string personalAccessToken); 22 | 23 | IEnumerable GetProjects(string connectionId, out Connection connection); 24 | 25 | void AddProjects(string connectionId, List projectsSelected); 26 | 27 | IEnumerable GetBuildDefinitions(string connectionId, string projectId, 28 | out Connection connection, out Project project); 29 | 30 | void AddBuildDefinitions(string connectionId, string projectId, List buildsSelected); 31 | 32 | IEnumerable GetBuilds(Connection connection, string projectName, int buildDefinitionId); 33 | 34 | IEnumerable GetLastBuildPerDefinition(); 35 | 36 | bool ProjectExistsInConfig(Guid connectionId, Guid projectId); 37 | 38 | bool BuildExistsInConfig(Guid connectionId, Guid projectId, int buildId); 39 | 40 | void DeleteConnection(string connectionId); 41 | 42 | void DeleteProject(string connectionId, string projectId); 43 | 44 | void DeleteBuild(string localId); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/Services/Interfaces/IPollingService.cs: -------------------------------------------------------------------------------- 1 | using static Tfs.BuildNotifications.Core.Services.PollingService; 2 | 3 | namespace Tfs.BuildNotifications.Core.Services.Interfaces 4 | { 5 | public interface IPollingService 6 | { 7 | void PollBuildNotifications(); 8 | 9 | event BuildNotificationEvent OnBuildStatusChange; 10 | event BuildPollCompletedEvent OnBuildPollComplete; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/Services/PollingService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using Tfs.BuildNotifications.Core.Extensions; 6 | using Tfs.BuildNotifications.Core.Services.Interfaces; 7 | using Tfs.BuildNotifications.Model; 8 | 9 | namespace Tfs.BuildNotifications.Core.Services 10 | { 11 | public class PollingService : IPollingService 12 | { 13 | private readonly IBuildConfigurationService _buildConfigurationService; 14 | 15 | private Timer _timer; 16 | private Dictionary _lastSeenBuilds; 17 | 18 | public delegate void BuildNotificationEvent(Build build); 19 | public delegate void BuildPollCompletedEvent(bool hasFailedBuilds); 20 | 21 | public event BuildNotificationEvent OnBuildStatusChange; 22 | public event BuildPollCompletedEvent OnBuildPollComplete; 23 | 24 | public TimeSpan PollInterval { get; set; } 25 | 26 | public PollingService(IBuildConfigurationService buildConfigurationService) 27 | { 28 | _buildConfigurationService = buildConfigurationService; 29 | } 30 | 31 | public void PollBuildNotifications() 32 | { 33 | _timer = new Timer(ProccessNotifications); 34 | 35 | StartTimer(TimeSpan.FromSeconds(0)); 36 | } 37 | 38 | #region Private Methods 39 | 40 | private void ProccessNotifications(object state) 41 | { 42 | StopTimer(); 43 | 44 | var buildUpdates = _buildConfigurationService.GetLastBuildPerDefinition(); 45 | 46 | if (_lastSeenBuilds != null) 47 | { 48 | // When polling after the first time we are interested in new builds or where the InProgress value has changed 49 | var changedBuilds = buildUpdates.Where(b => !_lastSeenBuilds.ContainsKey(b.Url) || _lastSeenBuilds[b.Url].InProgress != b.InProgress); 50 | foreach (var changedBuild in changedBuilds) 51 | { 52 | OnBuildStatusChange?.Invoke(changedBuild); 53 | 54 | } 55 | } 56 | _lastSeenBuilds = buildUpdates.ToDictionary(b => b.Url); 57 | 58 | OnBuildPollComplete?.Invoke(buildUpdates.Any(b => b.GetBuildResult() == BuildResult.Failed)); 59 | 60 | StartTimer(PollInterval); 61 | } 62 | 63 | private void StartTimer(TimeSpan delay) 64 | { 65 | _timer.Change(delay, PollInterval); 66 | } 67 | 68 | private void StopTimer() 69 | { 70 | _timer.Change(Timeout.Infinite, Timeout.Infinite); 71 | } 72 | 73 | #endregion 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/Tfs.BuildNotifications.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {12D1F80B-4CDA-460D-8AB4-5B0BDA802F9E} 8 | Library 9 | Properties 10 | Tfs.BuildNotifications.Core 11 | Tfs.BuildNotifications.Core 12 | v4.5.2 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll 36 | 37 | 38 | ..\packages\RestSharp.105.2.3\lib\net452\RestSharp.dll 39 | 40 | 41 | 42 | 43 | ..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {f8ef6d0f-a56e-4ba6-b6ba-df9c4a5f6792} 68 | Tfs.BuildNotifications.Common 69 | 70 | 71 | {fdf1e34f-79bb-4bbf-917d-6e9a6ff64a91} 72 | Tfs.BuildNotifications.Model 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Core/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Model/Build.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Tfs.BuildNotifications.Common.Extensions; 3 | 4 | namespace Tfs.BuildNotifications.Model 5 | { 6 | public class Build 7 | { 8 | public string DefinitionName { get; set; } 9 | public string LastRequestedBy { get; set; } 10 | public string LastRequestByProfileImageUrl { get; set; } 11 | public string Status { get; set; } 12 | public string Result { get; set; } 13 | public string Url { get; set; } 14 | 15 | public DateTime? LastFinished { get; set; } 16 | public DateTime? StartTime { get; set; } 17 | 18 | public bool InProgress => Status == "inProgress"; 19 | 20 | public Guid DefinitionLocalId { get; set; } 21 | 22 | public string DefinitionShortName => DefinitionName?.Shorten(42, "..."); 23 | public string LastFinishedDateString => LastFinished?.ToString(); 24 | public string StartTimeDateString => StartTime?.ToString(); 25 | 26 | public int BuildRunId { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Model/BuildConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Tfs.BuildNotifications.Model 5 | { 6 | public class BuildConfig 7 | { 8 | public BuildConfig() 9 | { 10 | Connections = new List(); 11 | } 12 | 13 | public List Connections { get; set; } 14 | } 15 | 16 | public class Connection : ICloneable 17 | { 18 | public Connection() 19 | { 20 | Projects = new List(); 21 | } 22 | 23 | public string TfsServerUrl { get; set; } 24 | public string PersonalAccessToken { get; set; } 25 | public string UserName { get; set; } 26 | public string Password { get; set; } 27 | public TfsServerDeployment TfsServerDeployment { get; set; } 28 | 29 | public List Projects { get; set; } 30 | 31 | public Guid Id { get; set; } 32 | 33 | public bool Broken { get; set; } 34 | 35 | public string LastConnectionError { get; set; } 36 | 37 | public object Clone() 38 | { 39 | return this.MemberwiseClone(); 40 | } 41 | } 42 | 43 | public class Project 44 | { 45 | public Project() 46 | { 47 | BuildDefinitions = new List(); 48 | } 49 | 50 | public string Name { get; set; } 51 | 52 | public Guid Id { get; set; } 53 | 54 | public List BuildDefinitions { get; set; } 55 | } 56 | 57 | public class BuildDefinition 58 | { 59 | public BuildDefinition() { } 60 | 61 | public int Id { get; set; } 62 | public string Name { get; set; } 63 | public string Url { get; set; } 64 | public Guid LocalId { get; set; } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Model/BuildStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Tfs.BuildNotifications.Model 2 | { 3 | public enum BuildResult 4 | { 5 | Unknown, 6 | Succeeded, 7 | Failed, 8 | Stopped, 9 | InProgress 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Model/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Tfs.BuildNotifications.Model")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("Tfs.BuildNotifications.Model")] 12 | [assembly: AssemblyCopyright("Copyright © 2017")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("fdf1e34f-79bb-4bbf-917d-6e9a6ff64a91")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Model/Tfs.BuildNotifications.Model.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {FDF1E34F-79BB-4BBF-917D-6E9A6FF64A91} 8 | Library 9 | Properties 10 | Tfs.BuildNotifications.Model 11 | Tfs.BuildNotifications.Model 12 | v4.5.2 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {f8ef6d0f-a56e-4ba6-b6ba-df9c4a5f6792} 53 | Tfs.BuildNotifications.Common 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Model/TfsServerDeployment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Tfs.BuildNotifications.Model 4 | { 5 | public enum TfsServerDeployment 6 | { 7 | OnPremises, 8 | OnlineVsts 9 | } 10 | 11 | public static class TfsServerDeploymentHelper 12 | { 13 | public static TfsServerDeployment ToTfsServerDeploymentHelperEnum(string value) 14 | { 15 | return (TfsServerDeployment)Enum.Parse(typeof(TfsServerDeployment), value); 16 | } 17 | 18 | public static string ToFriendlyName(this TfsServerDeployment tfsServerDeployment) 19 | { 20 | switch (tfsServerDeployment) 21 | { 22 | case TfsServerDeployment.OnlineVsts: 23 | return "Visual Studio Online"; 24 | 25 | case TfsServerDeployment.OnPremises: 26 | return "On Premises"; 27 | 28 | default: 29 | return "Unknown"; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Setup/Tfs.BuildNotifications.Setup.wixproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | x86 6 | 3.10 7 | 91521b2e-84c7-43ad-a951-5d631f438b2e 8 | 2.0 9 | Tfs.BuildNotifications.Setup 10 | Package 11 | 12 | 13 | 14 | 15 | bin\$(Configuration)\ 16 | obj\$(Configuration)\ 17 | Debug 18 | 19 | 20 | bin\$(Configuration)\ 21 | obj\$(Configuration)\ 22 | ICE69 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | C:\Program Files (x86)\WiX Toolset v3.11\bin\WixUtilExtension.dll 36 | WixUtilExtension 37 | 38 | 39 | C:\Program Files (x86)\WiX Toolset v3.11\bin\WixUIExtension.dll 40 | WixUIExtension 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 56 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Setup/banner.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Setup/banner.bmp -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Setup/dialog.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Setup/dialog.bmp -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Setup/license.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang2057{\fonttbl{\f0\fnil\fcharset0 Calibri;}} 2 | {\colortbl ;\red0\green0\blue255;} 3 | {\*\generator Riched20 10.0.17134}\viewkind4\uc1 4 | \pard\sa200\sl276\slmult1 {\f0\fs22\lang9{\field{\*\fldinst{HYPERLINK https://github.com/mattwendels/tfs-build-notifications }}{\fldrslt{https://github.com/mattwendels/tfs-build-notifications\ul0\cf0}}}}\f0\fs22\par 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\par 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\par 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\par 8 | } 9 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Setup/variables.wxi: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/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 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Infrastructure/Config/AppConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using Tfs.BuildNotifications.Tray.Infrastructure.Config.Interfaces; 4 | 5 | namespace Tfs.BuildNotifications.Tray.Infrastructure.Config 6 | { 7 | public class AppConfig : IAppConfig 8 | { 9 | public int WebsitePort => Convert.ToInt32(GetApplicationSetting("WebApp.Port")); 10 | 11 | public bool UseToolTipNotifications => Convert.ToBoolean(GetApplicationSetting("ToolTipNotifications.Enabled")); 12 | 13 | public bool UseTextToSpeech => Convert.ToBoolean(GetApplicationSetting("TextToSpeech.Enabled")); 14 | 15 | public bool NotifyNonSuccessfulBuildsOnly => 16 | Convert.ToBoolean(GetApplicationSetting("TrayNotifications.NonSuccessfulBuildsOnly")); 17 | 18 | public TimeSpan NotificationIntervalMinutes => 19 | TimeSpan.FromMinutes(Convert.ToInt32(GetApplicationSetting("Notifications.TimerIntervalMinutes"))); 20 | 21 | #region Private Methods 22 | 23 | private string GetApplicationSetting(string key) 24 | { 25 | return ConfigurationManager.AppSettings[key]; 26 | } 27 | 28 | #endregion 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Infrastructure/Config/Interfaces/IAppConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Tfs.BuildNotifications.Tray.Infrastructure.Config.Interfaces 4 | { 5 | public interface IAppConfig 6 | { 7 | int WebsitePort { get; } 8 | 9 | TimeSpan NotificationIntervalMinutes { get; } 10 | 11 | bool UseToolTipNotifications { get; } 12 | 13 | bool UseTextToSpeech { get; } 14 | 15 | bool NotifyNonSuccessfulBuildsOnly { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Infrastructure/Unity/Bootstrapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Practices.Unity; 2 | using Microsoft.Win32; 3 | using System; 4 | using Tfs.BuildNotifications.Common.Helpers; 5 | using Tfs.BuildNotifications.Common.Helpers.Interfaces; 6 | using Tfs.BuildNotifications.Common.Telemetry; 7 | using Tfs.BuildNotifications.Common.Telemetry.Interfaces; 8 | using Tfs.BuildNotifications.Core.Clients; 9 | using Tfs.BuildNotifications.Core.Clients.Interfaces; 10 | using Tfs.BuildNotifications.Core.Services; 11 | using Tfs.BuildNotifications.Core.Services.Interfaces; 12 | using Tfs.BuildNotifications.Tray.Infrastructure.Config; 13 | using Tfs.BuildNotifications.Tray.Infrastructure.Config.Interfaces; 14 | using Tfs.BuildNotifications.Tray.Services; 15 | using Tfs.BuildNotifications.Tray.Services.Interfaces; 16 | using Tfs.BuildNotifications.Web.Services; 17 | using Tfs.BuildNotifications.Web.Services.Interfaces; 18 | using Tfs.BuildNotifications.Web.SignalR; 19 | using Tfs.BuildNotifications.Web.SignalR.Interfaces; 20 | 21 | namespace Tfs.BuildNotifications.Tray.Infrastructure.Unity 22 | { 23 | public class Bootstrapper 24 | { 25 | private ILogService _logService = new LogService(); 26 | 27 | public IUnityContainer Bootstrap() 28 | { 29 | var container = new UnityContainer(); 30 | var appConfig = new AppConfig(); 31 | 32 | if (appConfig.UseToolTipNotifications || !IsWindows10()) 33 | { 34 | container.RegisterType("Tray"); 35 | } 36 | else 37 | { 38 | container.RegisterType("Tray"); 39 | } 40 | 41 | if (appConfig.UseTextToSpeech) 42 | { 43 | container.RegisterType("TextToSpeech"); 44 | } 45 | 46 | container 47 | .RegisterType() 48 | .RegisterType() 49 | .RegisterType() 50 | .RegisterType() 51 | .RegisterType(new InjectionProperty("PollInterval", appConfig.NotificationIntervalMinutes)) 52 | .RegisterType() 53 | .RegisterType() 54 | .RegisterType() 55 | .RegisterType(new InjectionProperty("WebsitePort", appConfig.WebsitePort)); 56 | 57 | 58 | return container; 59 | } 60 | 61 | #region Private Methods 62 | 63 | private bool IsWindows10() 64 | { 65 | try 66 | { 67 | var reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion"); 68 | 69 | var productName = (string)reg.GetValue("ProductName"); 70 | 71 | return productName.StartsWith("Windows 10"); 72 | } 73 | catch (Exception e) 74 | { 75 | _logService.Log("Failed to identify operating system version", e); 76 | 77 | return false; 78 | } 79 | } 80 | 81 | #endregion 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Lib/Windows.Foundation.FoundationContract.winmd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Lib/Windows.Foundation.FoundationContract.winmd -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Lib/Windows.Foundation.UniversalApiContract.winmd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Lib/Windows.Foundation.UniversalApiContract.winmd -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Practices.Unity; 2 | using Microsoft.Win32; 3 | using System; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Windows.Forms; 7 | using Tfs.BuildNotifications.Common.Telemetry.Interfaces; 8 | using Tfs.BuildNotifications.Core.Services.Interfaces; 9 | using Tfs.BuildNotifications.Tray.Infrastructure.Unity; 10 | using Tfs.BuildNotifications.Tray.Services.Interfaces; 11 | using Tfs.BuildNotifications.Web.Services.Interfaces; 12 | 13 | namespace Tfs.BuildNotifications.Tray 14 | { 15 | static class Program 16 | { 17 | private static ILogService _logService; 18 | 19 | static void Main() 20 | { 21 | var procName = Process.GetCurrentProcess().ProcessName; 22 | 23 | if (Process.GetProcesses().Count(p => p.ProcessName == procName) > 1) 24 | { 25 | MessageBox.Show("The TFS Build Notification application is already running.", "Information", MessageBoxButtons.OK, 26 | MessageBoxIcon.Information); 27 | 28 | return; 29 | } 30 | 31 | AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; 32 | 33 | Application.EnableVisualStyles(); 34 | Application.SetCompatibleTextRenderingDefault(false); 35 | 36 | var container = new Bootstrapper().Bootstrap(); 37 | 38 | _logService = container.Resolve(); 39 | 40 | var pollingService = container.Resolve(); 41 | var dashboardWebsite = container.Resolve(); 42 | var notificationServices = container.ResolveAll(); 43 | var buildConfigService = container.Resolve(); 44 | var tray = container.Resolve(); 45 | 46 | buildConfigService.Init(); 47 | 48 | pollingService.OnBuildPollComplete += tray.UpdateTrayStatus; 49 | pollingService.OnBuildStatusChange += dashboardWebsite.UpdateDashboardBuildStatus; 50 | 51 | foreach (var service in notificationServices) 52 | { 53 | pollingService.OnBuildStatusChange += service.NotifyBuildChange; 54 | } 55 | 56 | pollingService.PollBuildNotifications(); 57 | 58 | dashboardWebsite.StartWebsite(); 59 | 60 | Application.Run((ApplicationContext)tray); 61 | } 62 | 63 | #region Private Methods 64 | 65 | static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) 66 | { 67 | var exception = (Exception)e.ExceptionObject; 68 | 69 | _logService.Log("Unhandled application error", exception); 70 | 71 | MessageBox.Show($"TFS Build Notifications Error\r\n\r\nSorry, something seems to have gone wrong...\r\n\r\n{exception}\r\n\r\nPlease restart the application.", 72 | "TFS Build Notifications - Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 73 | } 74 | 75 | static void RegisterNotifications() 76 | { 77 | // ToDo: Fix and tidy up. 78 | var key = Registry.CurrentUser 79 | .CreateSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Notifications\\Settings\\TFS Build Notifications"); 80 | 81 | key.SetValue("ShowInActionCenter", 1); 82 | key.Close(); 83 | } 84 | 85 | #endregion 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("TFS Build Notifications")] 8 | [assembly: AssemblyDescription("A build notification and alert system for Microsoft Team Foundation Server.")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("Matt Wendels")] 11 | [assembly: AssemblyProduct("TFS Build Notifications")] 12 | [assembly: AssemblyCopyright("Copyright © Matt Wendels 2017")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("d121ef9f-ac72-49ae-8bb0-31f0924bfff5")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.4.1.0")] 35 | [assembly: AssemblyFileVersion("1.4.1.0")] 36 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Tfs.BuildNotifications.Tray.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Tfs.BuildNotifications.Tray.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 65 | /// 66 | internal static System.Drawing.Icon TfsIcon { 67 | get { 68 | object obj = ResourceManager.GetObject("TfsIcon", resourceCulture); 69 | return ((System.Drawing.Icon)(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 75 | /// 76 | internal static System.Drawing.Icon TfsIconBuildsFailing { 77 | get { 78 | object obj = ResourceManager.GetObject("TfsIconBuildsFailing", resourceCulture); 79 | return ((System.Drawing.Icon)(obj)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\Resources\tfs-icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | 125 | ..\Resources\tfs-icon-build-fail.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 126 | 127 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Tfs.BuildNotifications.Tray.Properties 12 | { 13 | 14 | 15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.3.0.0")] 17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 18 | 19 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 20 | 21 | public static Settings Default { 22 | get { 23 | return defaultInstance; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Resources/tfs-icon-build-fail.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Resources/tfs-icon-build-fail.ico -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Resources/tfs-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Resources/tfs-icon.ico -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Services/Interfaces/INotificationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Tfs.BuildNotifications.Model; 3 | 4 | namespace Tfs.BuildNotifications.Tray.Services.Interfaces 5 | { 6 | public interface INotificationService 7 | { 8 | void ShowGenericNotification(string title, string message, Action onActivation = null); 9 | 10 | void NotifyBuildChange(Build build); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Services/NotificationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Tfs.BuildNotifications.Core.Extensions; 4 | using Tfs.BuildNotifications.Model; 5 | using Tfs.BuildNotifications.Tray.Infrastructure.Config.Interfaces; 6 | 7 | namespace Tfs.BuildNotifications.Tray.Services 8 | { 9 | public abstract class NotificationService 10 | { 11 | protected const string DefaultNotificationIcon = "Content/Images/tfs-logo.png"; 12 | protected const string BuildStartedIcon = "Content/Images/play.png"; 13 | protected const string BuildCompletedIcon = "Content/Images/success.png"; 14 | protected const string BuildFailedIcon = "Content/Images/failed.png"; 15 | protected const string BuildStoppedIcon = "Content/Images/stopped.png"; 16 | protected const string BuildStatusUnknownIcon = "Content/Images/unknown.png"; 17 | 18 | private readonly IAppConfig _appConfig; 19 | 20 | public NotificationService(IAppConfig appConfig) 21 | { 22 | _appConfig = appConfig; 23 | } 24 | 25 | public void ShowGenericNotification(string title, string message, Action onActivation = null) 26 | { 27 | ShowNotification(title, message, DefaultNotificationIcon, onActivation); 28 | } 29 | 30 | public void NotifyBuildChange(Build build) 31 | { 32 | if (_appConfig.NotifyNonSuccessfulBuildsOnly) 33 | { 34 | if (build.GetBuildResult() != BuildResult.Succeeded) 35 | { 36 | ShowBuildStatusNotification(build); 37 | } 38 | } 39 | else 40 | { 41 | if (build.InProgress) 42 | { 43 | ShowBuildStartedNotification(build); 44 | } 45 | else 46 | { 47 | ShowBuildStatusNotification(build); 48 | } 49 | } 50 | 51 | } 52 | 53 | protected abstract void ShowNotification(string title, string message, string image, Action onActivation = null); 54 | 55 | #region Private Methods 56 | 57 | private void ShowBuildStartedNotification(Build build) 58 | { 59 | ShowNotification(build.DefinitionName, $"Build started by {build.LastRequestedBy}", BuildStartedIcon, 60 | () => Process.Start(build.Url)); 61 | } 62 | 63 | private void ShowBuildStatusNotification(Build build) 64 | { 65 | if (!build.InProgress) 66 | { 67 | var result = build.GetBuildResult(); 68 | 69 | Action onClick = () => Process.Start(build.Url); 70 | 71 | switch (result) 72 | { 73 | case BuildResult.Failed: 74 | ShowNotification(build.DefinitionName, $"{build.LastRequestedBy} broke the build.", BuildFailedIcon, 75 | onClick); 76 | break; 77 | 78 | case BuildResult.Stopped: 79 | ShowNotification(build.DefinitionName, "Build stopped. Click for details.", BuildStoppedIcon, 80 | onClick); 81 | break; 82 | 83 | case BuildResult.Succeeded: 84 | ShowNotification(build.DefinitionName, $"Build succeeded. Requested by {build.LastRequestedBy}", 85 | BuildCompletedIcon, onClick); 86 | break; 87 | 88 | case BuildResult.InProgress: 89 | ShowNotification(build.DefinitionName, $"Build in progress. Requested by {build.LastRequestedBy}", 90 | BuildCompletedIcon, onClick); 91 | break; 92 | 93 | default: 94 | ShowNotification(build.DefinitionName, "Build status unknown. Click for details.", 95 | BuildStatusUnknownIcon, onClick); 96 | break; 97 | } 98 | } 99 | } 100 | 101 | #endregion 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Services/TextToSpeechNotificationService.cs: -------------------------------------------------------------------------------- 1 | using NAudio.Wave; 2 | using System; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Speech.AudioFormat; 6 | using System.Speech.Synthesis; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Tfs.BuildNotifications.Tray.Infrastructure.Config.Interfaces; 10 | using Tfs.BuildNotifications.Tray.Services.Interfaces; 11 | 12 | namespace Tfs.BuildNotifications.Tray.Services 13 | { 14 | public class TextToSpeechNotificationService : NotificationService, INotificationService 15 | { 16 | public TextToSpeechNotificationService(IAppConfig appConfig) : base(appConfig) { } 17 | 18 | protected override void ShowNotification(string title, string message, string image, Action onActivation = null) 19 | { 20 | Speak(title); 21 | Speak(message); 22 | } 23 | 24 | private void Speak(string tts) 25 | { 26 | string fileName; 27 | 28 | fileName = Path.GetTempFileName(); 29 | fileName = Path.ChangeExtension(fileName, "wav"); 30 | 31 | using (var synth = new SpeechSynthesizer()) 32 | { 33 | synth.SelectVoice(GetVoiceName(synth)); 34 | 35 | // Configure the audio output. 36 | synth.SetOutputToWaveFile(fileName, 37 | new SpeechAudioFormatInfo(32000, AudioBitsPerSample.Sixteen, AudioChannel.Stereo)); 38 | 39 | synth.Speak(tts); 40 | } 41 | 42 | var device = -1; 43 | 44 | if (device == -1) 45 | { 46 | var devices = WaveOut.DeviceCount; 47 | 48 | Parallel.For(0, devices, (deviceNumber, state) => 49 | { 50 | try 51 | { 52 | PlayOnDevice(fileName, deviceNumber); 53 | } 54 | catch (Exception) { } 55 | }); 56 | } 57 | else 58 | { 59 | PlayOnDevice(fileName, device); 60 | } 61 | 62 | File.Delete(fileName); 63 | } 64 | 65 | private string GetVoiceName(SpeechSynthesizer synth) 66 | { 67 | var voices = synth.GetInstalledVoices(); 68 | 69 | // Use the last one at the moment. ToDo: set in app.config? 70 | return voices.Last().VoiceInfo.Name; 71 | } 72 | 73 | private static void PlayOnDevice(string fileName, int device) 74 | { 75 | var waveOut = new WaveOutEvent(); 76 | 77 | waveOut.DeviceNumber = device; 78 | 79 | WaveStream reader; 80 | 81 | if (fileName.EndsWith("mp3")) 82 | { 83 | reader = new Mp3FileReader(fileName); 84 | } 85 | else 86 | { 87 | reader = new WaveFileReader(fileName); 88 | } 89 | 90 | waveOut.Init(reader); 91 | waveOut.Play(); 92 | 93 | while (waveOut.PlaybackState != PlaybackState.Stopped) 94 | { 95 | Thread.Sleep(20); 96 | } 97 | 98 | waveOut.Stop(); 99 | 100 | reader.Dispose(); 101 | waveOut.Dispose(); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Services/ToastNotificationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Tfs.BuildNotifications.Tray.Infrastructure.Config.Interfaces; 4 | using Tfs.BuildNotifications.Tray.Services.Interfaces; 5 | using Windows.UI.Notifications; 6 | 7 | namespace Tfs.BuildNotifications.Tray.Services 8 | { 9 | public class ToastNotificationService : NotificationService, INotificationService 10 | { 11 | public ToastNotificationService(IAppConfig appConfig) : base(appConfig) { } 12 | 13 | protected override void ShowNotification(string title, string message, string image, Action onActivation = null) 14 | { 15 | var toastXml = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastImageAndText04); 16 | 17 | var stringElements = toastXml.GetElementsByTagName("text"); 18 | 19 | stringElements[0].AppendChild(toastXml.CreateTextNode(title)); 20 | stringElements[1].AppendChild(toastXml.CreateTextNode(message)); 21 | 22 | // Specify the absolute path to an image. 23 | var imagePath = "file:///" + Path.GetFullPath(image); 24 | var imageElements = toastXml.GetElementsByTagName("image"); 25 | 26 | imageElements[0].Attributes.GetNamedItem("src").NodeValue = imagePath; 27 | 28 | var toast = new ToastNotification(toastXml); 29 | 30 | if (onActivation != null) 31 | { 32 | toast.Activated += (sender, e) => ToastActivated(sender, e, onActivation); 33 | } 34 | 35 | ToastNotificationManager.CreateToastNotifier("TFS Build Notifications").Show(toast); 36 | } 37 | 38 | private void ToastActivated(ToastNotification sender, object e, Action action) 39 | { 40 | action.Invoke(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Services/ToolTipNotificationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | using Tfs.BuildNotifications.Tray.Infrastructure.Config.Interfaces; 4 | using Tfs.BuildNotifications.Tray.Services.Interfaces; 5 | 6 | namespace Tfs.BuildNotifications.Tray.Services 7 | { 8 | public class ToolTipNotificationService : NotificationService, INotificationService 9 | { 10 | public ToolTipNotificationService(IAppConfig appConfig) : base(appConfig) { } 11 | 12 | protected override void ShowNotification(string title, string message, string image, Action onActivation = null) 13 | { 14 | TrayIconApplicationContext.TrayIcon.ShowBalloonTip(0, title, message, GetToolTipIcon(image)); 15 | } 16 | 17 | private ToolTipIcon GetToolTipIcon(string image) 18 | { 19 | switch (image) 20 | { 21 | case BuildStoppedIcon: 22 | case BuildStatusUnknownIcon: 23 | return ToolTipIcon.Info; 24 | 25 | case BuildFailedIcon: 26 | return ToolTipIcon.Error; 27 | 28 | default: 29 | return ToolTipIcon.Info; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/Tfs.BuildNotifications.Tray.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {D121EF9F-AC72-49AE-8BB0-31F0924BFFF5} 8 | WinExe 9 | Tfs.BuildNotifications.Tray 10 | TFS Build Notifications 11 | v4.5.2 12 | 512 13 | true 14 | 15 | 16 | 17 | publish\ 18 | true 19 | Disk 20 | false 21 | Foreground 22 | 7 23 | Days 24 | false 25 | false 26 | true 27 | 0 28 | 1.0.0.%2a 29 | false 30 | false 31 | true 32 | 33 | 34 | AnyCPU 35 | true 36 | full 37 | false 38 | bin\Debug\ 39 | DEBUG;TRACE 40 | prompt 41 | 4 42 | 7 43 | 44 | 45 | 8.0 46 | 47 | 48 | AnyCPU 49 | pdbonly 50 | true 51 | bin\Release\ 52 | TRACE 53 | prompt 54 | 4 55 | 56 | 57 | app.manifest 58 | 59 | 60 | Resources\tfs-icon.ico 61 | 62 | 63 | 64 | ..\packages\FluentValidation.3.4.0.0\lib\Net40\FluentValidation.dll 65 | 66 | 67 | ..\packages\Microsoft.AspNet.SignalR.Core.2.2.2\lib\net45\Microsoft.AspNet.SignalR.Core.dll 68 | 69 | 70 | ..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll 71 | 72 | 73 | ..\packages\Microsoft.Owin.Host.HttpListener.3.1.0\lib\net45\Microsoft.Owin.Host.HttpListener.dll 74 | 75 | 76 | ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll 77 | 78 | 79 | ..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll 80 | 81 | 82 | ..\packages\Unity.4.0.1\lib\net45\Microsoft.Practices.Unity.dll 83 | 84 | 85 | ..\packages\Unity.4.0.1\lib\net45\Microsoft.Practices.Unity.Configuration.dll 86 | 87 | 88 | ..\packages\Unity.4.0.1\lib\net45\Microsoft.Practices.Unity.RegistrationByConvention.dll 89 | 90 | 91 | ..\packages\Nancy.1.4.4\lib\net40\Nancy.dll 92 | 93 | 94 | ..\packages\Nancy.Validation.FluentValidation.1.4.1\lib\net40\Nancy.Validation.FluentValidation.dll 95 | 96 | 97 | ..\packages\NAudio.1.8.4\lib\net35\NAudio.dll 98 | 99 | 100 | ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll 101 | 102 | 103 | ..\packages\Owin.1.0\lib\net40\Owin.dll 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | False 122 | Lib\Windows.Foundation.FoundationContract.winmd 123 | 124 | 125 | False 126 | Lib\Windows.Foundation.UniversalApiContract.winmd 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | ResXFileCodeGenerator 143 | Resources.Designer.cs 144 | Designer 145 | 146 | 147 | True 148 | Resources.resx 149 | True 150 | 151 | 152 | Always 153 | 154 | 155 | Designer 156 | 157 | 158 | SettingsSingleFileGenerator 159 | Settings.Designer.cs 160 | 161 | 162 | True 163 | Settings.settings 164 | True 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | Always 173 | 174 | 175 | 176 | 177 | {f8ef6d0f-a56e-4ba6-b6ba-df9c4a5f6792} 178 | Tfs.BuildNotifications.Common 179 | 180 | 181 | {12d1f80b-4cda-460d-8ab4-5b0bda802f9e} 182 | Tfs.BuildNotifications.Core 183 | 184 | 185 | {fdf1e34f-79bb-4bbf-917d-6e9a6ff64a91} 186 | Tfs.BuildNotifications.Model 187 | 188 | 189 | {4db8c252-0575-4436-8595-7247c3a667ca} 190 | Tfs.BuildNotifications.Web 191 | 192 | 193 | 194 | 195 | Always 196 | 197 | 198 | 199 | 200 | False 201 | Microsoft .NET Framework 4.5.2 %28x86 and x64%29 202 | true 203 | 204 | 205 | False 206 | .NET Framework 3.5 SP1 207 | false 208 | 209 | 210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/TrayIconApplicationContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Reflection; 4 | using System.Windows.Forms; 5 | using Microsoft.Practices.Unity; 6 | using Tfs.BuildNotifications.Core.Services.Interfaces; 7 | using Tfs.BuildNotifications.Tray.Infrastructure.Config.Interfaces; 8 | using Tfs.BuildNotifications.Tray.Properties; 9 | using Tfs.BuildNotifications.Tray.Services.Interfaces; 10 | 11 | namespace Tfs.BuildNotifications.Tray 12 | { 13 | internal interface ITrayIconApplicationContext 14 | { 15 | void UpdateTrayStatus(bool hasAnyFailedBuilds); 16 | } 17 | 18 | internal class TrayIconApplicationContext : ApplicationContext, ITrayIconApplicationContext 19 | { 20 | public static NotifyIcon TrayIcon; 21 | 22 | private readonly INotificationService _notificationService; 23 | private readonly IBuildConfigurationService _buildConfigurationService; 24 | private readonly IAppConfig _appConfig; 25 | 26 | public TrayIconApplicationContext([Dependency("Tray")]INotificationService notificationService, 27 | IBuildConfigurationService buildConfigurationService, IAppConfig appConfig) 28 | { 29 | _notificationService = notificationService; 30 | _buildConfigurationService = buildConfigurationService; 31 | _appConfig = appConfig; 32 | 33 | TrayIcon = new NotifyIcon() 34 | { 35 | Icon = Resources.TfsIcon, 36 | Visible = true, 37 | Text = "TFS Build Notifications", 38 | 39 | ContextMenu = new ContextMenu(new MenuItem[] 40 | { 41 | new MenuItem("Open Dashboard", OpenDashboard), 42 | new MenuItem("Exit", Exit) 43 | }) 44 | }; 45 | 46 | TrayIcon.MouseClick += OnMouseClick; 47 | 48 | if (_buildConfigurationService.HasAnyMonitoredBuilds()) 49 | { 50 | _notificationService.ShowGenericNotification("TFS Build Notifications", "Build notifications enabled.", 51 | () => Process.Start($"http://localhost:{_appConfig.WebsitePort}")); 52 | } 53 | else 54 | { 55 | _notificationService.ShowGenericNotification("TFS Build Notifications", 56 | "No build notifications configured. Click here to get started.", 57 | () => Process.Start($"http://localhost:{_appConfig.WebsitePort}")); 58 | } 59 | } 60 | 61 | public void UpdateTrayStatus(bool hasAnyFailedBuilds) 62 | { 63 | if (hasAnyFailedBuilds) 64 | { 65 | TrayIcon.Icon = Resources.TfsIconBuildsFailing; 66 | TrayIcon.Text = "TFS Build Notifications - Build(s) failing"; 67 | } 68 | else 69 | { 70 | TrayIcon.Icon = Resources.TfsIcon; 71 | TrayIcon.Text = "TFS Build Notifications"; 72 | } 73 | } 74 | 75 | #region Events 76 | 77 | void Exit(object sender, EventArgs e) 78 | { 79 | // Hide tray icon, otherwise it will remain shown until user mouses over it. 80 | TrayIcon.Visible = false; 81 | 82 | Application.Exit(); 83 | } 84 | 85 | void OpenDashboard(object sender, EventArgs e) 86 | { 87 | Process.Start($"http://localhost:{_appConfig.WebsitePort}"); 88 | } 89 | 90 | void OnApplicationExit(object sender, EventArgs e) 91 | { 92 | TrayIcon.Visible = false; 93 | } 94 | 95 | void OnMouseClick(object sender, MouseEventArgs e) 96 | { 97 | if (e.Button == MouseButtons.Left) 98 | { 99 | var mi = typeof(NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.Instance | BindingFlags.NonPublic); 100 | 101 | mi.Invoke(TrayIcon, null); 102 | } 103 | } 104 | 105 | #endregion 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 59 | 60 | 61 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Tray/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/close.png -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/delete.png -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/failed.png -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/minus.png -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/play.png -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/plus.png -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/stopped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/stopped.png -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/success.png -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/tfs-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/tfs-logo.png -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Images/unknown.png -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Scripts/dashboard.js: -------------------------------------------------------------------------------- 1 | // Quick highlight and fade... 2 | jQuery.fn.highlight = function () { 3 | 4 | $(this).each(function () { 5 | var el = $(this); 6 | $("
") 7 | .width(el.outerWidth()) 8 | .height(el.outerHeight()) 9 | .css({ 10 | "position": "absolute", 11 | "left": el.offset().left, 12 | "top": el.offset().top, 13 | "background-color": "#ffff99", 14 | "opacity": ".7", 15 | "z-index": "9999999" 16 | }).appendTo('body').fadeOut(2000).queue(function () { $(this).remove(); }); 17 | }); 18 | } 19 | 20 | var dashboardHubProxy = $.connection.dashboardHub; 21 | var runningBuildsCount = 0; 22 | 23 | var dashboardLib = function (options) { 24 | 25 | var init = function () { 26 | 27 | // Bind server calls. 28 | dashboardHubProxy.client.buildNotification = function (data) { 29 | updateUiForBuildChange(data); 30 | }; 31 | 32 | $.connection.hub.start() 33 | .done(function () { 34 | console.log('SignalR connected. Connection ID=' + $.connection.hub.id); 35 | 36 | // Get a list of running builds and update UI (if builds are running during dashboard load). 37 | dashboardHubProxy.server.getRunningBuilds($.connection.hub.id); 38 | }) 39 | .fail(function () { console.log('SignalR failed to connect.'); }); 40 | 41 | function updateUiForBuildChange(data) { 42 | 43 | var buildNotification = $.parseJSON(data); 44 | var dashboardBuild = $('*[data-local-id="' + buildNotification.DefinitionLocalId + '"]').not('*[data-build-running]', 'true'); 45 | var connectionId = dashboardBuild.attr('data-connection-id'); 46 | var projectId = dashboardBuild.attr('data-project-id'); 47 | 48 | // Update dashboard definition (to set as now running etc.) 49 | $.ajax({ 50 | type: 'get', 51 | url: '/buildsummary', 52 | data: { 53 | connectionId: connectionId, 54 | projectId: projectId, 55 | localBuildId: buildNotification.DefinitionLocalId 56 | }, 57 | success: function (data) { 58 | 59 | var updatedBuild = ($(data)); 60 | 61 | dashboardBuild.replaceWith(updatedBuild); 62 | 63 | // Add to 'Running Now' section. 64 | if (buildNotification.InProgress) { 65 | 66 | if (!buildAlreadyMarkedAsRunning(buildNotification)) { 67 | 68 | runningBuildsCount++; 69 | 70 | var runningNow = $(data); 71 | 72 | runningNow.hide(); 73 | runningNow.attr('data-build-running', 'true'); 74 | runningNow.find(options.removeBuildClass).remove(); 75 | 76 | options.buildsRunningContainer.append(runningNow); 77 | 78 | runningNow.fadeIn(400, function () { 79 | checkAnyBuildsRunning(); 80 | setTimeout(runningNow.highlight(), 1000); 81 | 82 | }); 83 | } 84 | } 85 | else { 86 | 87 | runningBuildsCount--; 88 | 89 | // Remove running now panel and replace with updated build result. 90 | var runningBuildPanel = options.buildsRunningContainer 91 | .find('[data-local-id="' + buildNotification.DefinitionLocalId + '"][data-last-run-id="' + buildNotification.BuildRunId + '"]'); 92 | 93 | var completedBuild = $(data); 94 | 95 | completedBuild.find(options.removeBuildClass).remove(); 96 | 97 | runningBuildPanel.replaceWith(completedBuild); 98 | 99 | var delayPanelRemove = options.buildsRunningContainer 100 | .find('[data-local-id="' + buildNotification.DefinitionLocalId + '"][data-last-run-id="' + buildNotification.BuildRunId + '"]'); 101 | 102 | // Display result for 5 seconds then remove. 103 | setTimeout(function () { 104 | 105 | delayPanelRemove.fadeOut(400, function () { 106 | checkAnyBuildsRunning(); 107 | }); 108 | }, 5000); 109 | } 110 | } 111 | }); 112 | } 113 | }; 114 | 115 | var checkAnyBuildsRunning = function () { 116 | 117 | if (runningBuildsCount > 0) { 118 | options.noBuildsRunningMessage.hide(); 119 | } 120 | else { 121 | options.noBuildsRunningMessage.show(); 122 | 123 | // Running build panel out of sync, refresh the page. 124 | if (options.buildsRunningContainer.find('[data-build-running="true"]').length > 0) { 125 | window.location.reload(); 126 | } 127 | } 128 | }; 129 | 130 | var buildAlreadyMarkedAsRunning = function (buildNotification) { 131 | 132 | return result = options.buildsRunningContainer 133 | .find('*[data-local-id="' + buildNotification.DefinitionLocalId + '"][data-last-run-id="' + buildNotification.BuildRunId + '"]') 134 | .length > 0; 135 | }; 136 | 137 | init(); 138 | } -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/Styles/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #f9f9f9; 3 | font-family: "-apple-system",BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Helvetica,Ubuntu,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 4 | font-size: 14px; 5 | } 6 | 7 | p { 8 | margin-bottom: 10px; 9 | } 10 | 11 | strong { 12 | font-weight: 500; 13 | } 14 | 15 | .container { 16 | width: 1080px; 17 | margin: 0 auto; 18 | border: 1px solid #dcdcdc; 19 | background: #fff; 20 | padding: 10px 20px; 21 | border-radius: 0.5em; 22 | margin-bottom: 15px; 23 | } 24 | 25 | h1, h2, h3 { 26 | font-weight: 200; 27 | margin: 7px 0 15px 0; 28 | } 29 | 30 | a, a:visited { 31 | color: #0078d7; 32 | } 33 | 34 | .header-logo { 35 | width: 50px; 36 | } 37 | 38 | .header img, .header h1 { 39 | display: inline-block; 40 | } 41 | 42 | .header img { 43 | float: left; 44 | margin-top: 15px; 45 | } 46 | 47 | .header h1 { 48 | margin: 20px 0 20px 15px; 49 | } 50 | 51 | .info-box { 52 | padding: 10px; 53 | color: #31708f; 54 | background-color: #d9edf7; 55 | border: 1px solid #bce8f1; 56 | border-radius: 0.3em; 57 | margin-bottom: 10px; 58 | } 59 | 60 | .info-box p { 61 | margin: 0; 62 | } 63 | 64 | .new-connection, .radio { 65 | margin: 15px 0; 66 | } 67 | 68 | label { 69 | font-weight: 500; 70 | display: inline-block; 71 | width: 165px; 72 | } 73 | 74 | input[type="text"], input[type="password"] { 75 | padding: 10px; 76 | border-radius: 0.5em; 77 | border: 1px solid #dcdcdc; 78 | margin-bottom: 15px; 79 | width: 295px; 80 | font-family: "-apple-system",BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Helvetica,Ubuntu,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 81 | } 82 | 83 | input[type="submit"], .button { 84 | background-color: #0078d7; 85 | color: #fff; 86 | border: none; 87 | padding: 10px; 88 | font-family: "-apple-system",BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Helvetica,Ubuntu,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 89 | margin-bottom: 10px; 90 | } 91 | 92 | .field-validation-error, .field-validation-error p { 93 | color: red; 94 | } 95 | 96 | .radio label, .radio input[type="radio"] { 97 | display: inline; 98 | font-weight: normal; 99 | } 100 | 101 | .radio label { 102 | margin-right: 20px; 103 | } 104 | 105 | .clearfix { 106 | clear: both; 107 | } 108 | 109 | .exception { 110 | font-family: Consolas; 111 | color: #808080; 112 | } 113 | 114 | .selection-list { 115 | margin: 25px 0; 116 | } 117 | 118 | .selection-list .select-selection { 119 | padding: 10px 0; 120 | border-bottom: 1px solid #dcdcdc; 121 | width: 75%; 122 | } 123 | 124 | .selection-list .select-selection label { 125 | font-weight: normal; 126 | width: 90%; 127 | } 128 | 129 | .selection-disabled { 130 | color: #808080; 131 | } 132 | 133 | .connection { 134 | margin-bottom: 40px; 135 | } 136 | 137 | .connection h3 { 138 | border-bottom: 1px solid #808080; 139 | padding-bottom: 10px; 140 | } 141 | 142 | .connection h3 a, .connection h3 a,:visited, .connection h3 a:hover { 143 | text-decoration: none; 144 | color: #000; 145 | } 146 | 147 | .build-summary { 148 | padding: 7px; 149 | border: 1px solid #dcdcdc; 150 | border-radius: 0.3em; 151 | float: left; 152 | margin: 0 10px 10px 0; 153 | height: 96px; 154 | width: 330px; 155 | position: relative; 156 | } 157 | 158 | .build-summary .removeBuild img { 159 | top: 6px; 160 | right: 6px; 161 | position: absolute; 162 | } 163 | 164 | .build-summary p:first-child { 165 | margin-top: 2px; 166 | } 167 | 168 | .build-summary .status-image { 169 | float: left; 170 | margin-right: 5px; 171 | } 172 | 173 | .build-summary .status-image img { 174 | width: 24px; 175 | height: 24px; 176 | } 177 | 178 | .build-summary .build-info p { 179 | font-size: 12px; 180 | margin: 0 0 3px 0; 181 | } 182 | .build-summary .build-info p strong { 183 | color: #404040; 184 | } 185 | 186 | .build-summary a, .build-summary a:visited { 187 | text-decoration: none; 188 | color: #000; 189 | } 190 | 191 | .build-summary p a:hover { 192 | text-decoration: underline; 193 | } 194 | 195 | .history-other, .history-success, .history-fail, .history-running { 196 | width: 12px; 197 | height: 12px; 198 | background-color: gray; 199 | display: inline-block; 200 | border-radius: 0.2em; 201 | } 202 | 203 | .history-success { 204 | background-color: #60BD68; 205 | } 206 | 207 | .history-fail { 208 | background-color: #F15854; 209 | } 210 | 211 | .history-running { 212 | background-color: #008DE2; 213 | } 214 | 215 | .build-history { 216 | margin-top: 5px; 217 | } 218 | 219 | .select-deselect a, .edit-links a, .go-back a { 220 | font-size: 12px; 221 | } 222 | 223 | #buildsRunning { 224 | padding: 10px 0px 10px 10px; 225 | border: 1px solid #008DE2; 226 | border-radius: 0.3em; 227 | margin: 30px 0px; 228 | overflow: auto; 229 | } 230 | 231 | #noBuildsRunningMessage { 232 | font-style: italic; 233 | font-size: 12px; 234 | } 235 | 236 | .build-running-container { 237 | height: auto; 238 | } 239 | 240 | .conn-close { 241 | float: right; 242 | margin: 2px 3px 0 0; 243 | } 244 | 245 | .edit-links { 246 | margin-bottom: 15px; 247 | } 248 | 249 | .no-script { 250 | background: #fffbec; 251 | padding: 10px; 252 | border: 1px solid #FFE793; 253 | margin: 10px 0 25px 0; 254 | } 255 | 256 | .no-script p { 257 | margin: 0; 258 | } 259 | 260 | .error { 261 | color: #a94442; 262 | background-color: #f2dede; 263 | border: 1px solid #ebccd1; 264 | padding: 0 10px; 265 | border-radius: 0.3em; 266 | margin-bottom: 10px; 267 | } 268 | 269 | .notes { 270 | font-size: 12px; 271 | margin: -10px 0 20px 170px; 272 | } 273 | 274 | .notes p { 275 | margin: 0 0 7px 0; 276 | } -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Content/favicon.ico -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Nancy/Configuration/NancyCustomBootstrapper.cs: -------------------------------------------------------------------------------- 1 | using Nancy; 2 | using Nancy.Bootstrapper; 3 | using Nancy.TinyIoc; 4 | 5 | namespace Tfs.BuildNotifications.Web.Nancy.Configuration 6 | { 7 | public class NancyCustomBootstrapper : DefaultNancyBootstrapper 8 | { 9 | protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) 10 | { 11 | // Allow custom error handlers in release configuration. 12 | // https://github.com/NancyFx/Nancy/issues/2052 13 | StaticConfiguration.DisableErrorTraces = false; 14 | 15 | base.ApplicationStartup(container, pipelines); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Nancy/Configuration/NancyRazorConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Nancy.ViewEngines.Razor; 2 | using System.Collections.Generic; 3 | 4 | namespace Tfs.BuildNotifications.Web.Nancy.Configuration 5 | { 6 | public class NancyRazorConfiguration : IRazorConfiguration 7 | { 8 | public bool AutoIncludeModelNamespace => true; 9 | 10 | public IEnumerable GetAssemblyNames() 11 | { 12 | yield return "Tfs.BuildNotifications.Web"; 13 | yield return "Tfs.BuildNotifications.Model"; 14 | yield return "Tfs.BuildNotifications.Core"; 15 | yield return "Tfs.BuildNotifications.Common"; 16 | } 17 | 18 | public IEnumerable GetDefaultNamespaces() 19 | { 20 | yield return "Nancy.Validation"; 21 | yield return "System.Globalization"; 22 | yield return "System.Collections.Generic"; 23 | yield return "System.Linq"; 24 | yield return "Tfs.BuildNotifications.Web.ViewModels"; 25 | yield return "Tfs.BuildNotifications.Model"; 26 | yield return "Tfs.BuildNotifications.Core.Extensions"; 27 | yield return "Tfs.BuildNotifications.Common.Extensions"; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Nancy/Handlers/PageNotFoundHandler.cs: -------------------------------------------------------------------------------- 1 | using Nancy; 2 | using Nancy.ErrorHandling; 3 | using Nancy.ViewEngines; 4 | 5 | namespace Tfs.BuildNotifications.Web.Nancy.Handlers 6 | { 7 | public class PageNotFoundHandler : DefaultViewRenderer, IStatusCodeHandler 8 | { 9 | public PageNotFoundHandler(IViewFactory factory) : base(factory) { } 10 | 11 | public bool HandlesStatusCode(HttpStatusCode statusCode, NancyContext context) 12 | { 13 | return statusCode == HttpStatusCode.NotFound; 14 | } 15 | 16 | public void Handle(HttpStatusCode statusCode, NancyContext context) 17 | { 18 | var response = RenderView(context, "Views/NotFound.cshtml"); 19 | 20 | response.StatusCode = HttpStatusCode.NotFound; 21 | 22 | context.Response = response; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Nancy/Handlers/ServerErrorHandler.cs: -------------------------------------------------------------------------------- 1 | using Nancy; 2 | using Nancy.ErrorHandling; 3 | using Nancy.Extensions; 4 | using Nancy.ViewEngines; 5 | using Tfs.BuildNotifications.Common.Telemetry.Interfaces; 6 | 7 | namespace Tfs.BuildNotifications.Web.Nancy.Handlers 8 | { 9 | public class ServerErrorHandler : DefaultViewRenderer, IStatusCodeHandler 10 | { 11 | public ServerErrorHandler(IViewFactory factory) : base(factory) { } 12 | 13 | public bool HandlesStatusCode(HttpStatusCode statusCode, NancyContext context) 14 | { 15 | return statusCode == HttpStatusCode.InternalServerError; 16 | } 17 | 18 | public void Handle(HttpStatusCode statusCode, NancyContext context) 19 | { 20 | var e = context.GetException(); 21 | 22 | LoggingErrorHandler.LogService.Log($"Nancy server error.", e); 23 | 24 | var response = RenderView(context, "Views/Error.cshtml", e); 25 | 26 | response.StatusCode = HttpStatusCode.InternalServerError; 27 | 28 | context.Response = response; 29 | } 30 | } 31 | 32 | public static class LoggingErrorHandler 33 | { 34 | public static ILogService LogService; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Nancy/Modules/ConnectionModule.cs: -------------------------------------------------------------------------------- 1 | using Nancy; 2 | using Nancy.ModelBinding; 3 | using Nancy.Validation; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Tfs.BuildNotifications.Core.Exceptions; 8 | using Tfs.BuildNotifications.Core.Services.Interfaces; 9 | using Tfs.BuildNotifications.Model; 10 | using Tfs.BuildNotifications.Web.ViewModels; 11 | 12 | namespace Tfs.BuildNotifications.Web.Nancy.Modules 13 | { 14 | public class ConnectionModule : NancyModule 15 | { 16 | private readonly IBuildConfigurationService _buildConfigurationService; 17 | 18 | public ConnectionModule(IBuildConfigurationService buildConfigurationService) 19 | { 20 | _buildConfigurationService = buildConfigurationService; 21 | 22 | #region /addconnection 23 | 24 | Get["/addconnection"] = _ => 25 | { 26 | return View["Views/AddConnection.cshtml", new AddEditConnectionViewModel 27 | { 28 | NewConfiguration = !_buildConfigurationService.HasConfiguration() 29 | }]; 30 | }; 31 | 32 | Post["/addconnection"] = _ => 33 | { 34 | var model = this.Bind(); 35 | var validationResult = this.Validate(model); 36 | 37 | if (!validationResult.IsValid) 38 | { 39 | model.Errors.AddRange(validationResult.Errors.SelectMany(e => e.Value).Select(y => y.ErrorMessage)); 40 | 41 | return View["Views/AddConnection.cshtml", model]; 42 | } 43 | 44 | try 45 | { 46 | _buildConfigurationService.AddConnection(model.TfsServerUrl, model.TfsServerLocation, model.UserName, 47 | model.Password, model.PersonalAccessToken); 48 | } 49 | catch (ServiceValidationException s) 50 | { 51 | model.Errors.AddRange(s.ServiceErrors); 52 | 53 | return View["Views/AddConnection.cshtml", model]; 54 | } 55 | 56 | return Response.AsRedirect("/"); 57 | }; 58 | 59 | #endregion 60 | 61 | #region /editconnection 62 | 63 | Get["/editconnection/{id}"] = x => 64 | { 65 | var connection = _buildConfigurationService.GetBuildConfig().Connections.FirstOrDefault(c => c.Id.ToString() == x.id); 66 | 67 | if (connection != null) 68 | { 69 | var viewModel = new AddEditConnectionViewModel 70 | { 71 | NewConfiguration = false, 72 | UserName = connection.UserName, 73 | TfsServerUrl = connection.TfsServerUrl, 74 | TfsServerLocation = connection.TfsServerDeployment.ToString() 75 | }; 76 | 77 | return View["Views/EditConnection.cshtml", viewModel]; 78 | } 79 | 80 | return Response.AsRedirect("/"); 81 | }; 82 | 83 | Post["/editconnection/{id}"] = x => 84 | { 85 | var model = this.Bind(); 86 | var validationResult = this.Validate(model); 87 | 88 | if (!validationResult.IsValid) 89 | { 90 | model.Errors.AddRange(validationResult.Errors.SelectMany(e => e.Value).Select(y => y.ErrorMessage)); 91 | 92 | return View["Views/EditConnection.cshtml", model]; 93 | } 94 | 95 | try 96 | { 97 | _buildConfigurationService.EditConnection(x.id, model.TfsServerUrl, model.TfsServerLocation, 98 | model.UserName, model.Password, model.PersonalAccessToken); 99 | } 100 | catch (ServiceValidationException s) 101 | { 102 | model.Errors.AddRange(s.ServiceErrors); 103 | 104 | return View["Views/EditConnection.cshtml", model]; 105 | } 106 | 107 | return Response.AsRedirect("/"); 108 | }; 109 | 110 | #endregion 111 | 112 | #region /deleteconnection 113 | 114 | Post["/deleteconnection"] = _ => 115 | { 116 | _buildConfigurationService.DeleteConnection((string)Request.Form["ConnectionId"]); 117 | 118 | return Response.AsRedirect("/"); 119 | }; 120 | 121 | #endregion 122 | 123 | #region /addproject 124 | 125 | Get["/addproject/{id}"] = x => 126 | { 127 | string id = x.id; 128 | 129 | try 130 | { 131 | var projects = _buildConfigurationService.GetProjects(id, out var connection); 132 | 133 | var model = new AddProjectViewModel 134 | { 135 | Connection = connection, 136 | ProjectSelections = projects.Select(p => new ProjectSelectionModel 137 | { 138 | Id = p.Id, Name = p.Name, 139 | Disabled = _buildConfigurationService.ProjectExistsInConfig(connection.Id, p.Id) 140 | 141 | }).ToList() 142 | }; 143 | 144 | return View["Views/AddProject.cshtml", model]; 145 | } 146 | catch (InvalidOperationException) 147 | { 148 | return new NotFoundResponse(); 149 | } 150 | }; 151 | 152 | Post["/addproject"] = x => 153 | { 154 | var connectionId = (string)Request.Form["ConnectionId"]; 155 | var selections = this.Bind>(); 156 | 157 | _buildConfigurationService.AddProjects(connectionId, 158 | selections.Where(s => s.Selected).Select(s => new Project { Id = s.Id, Name = s.Name }).ToList()); 159 | 160 | return Response.AsRedirect("/"); 161 | }; 162 | 163 | #endregion 164 | 165 | #region /deleteproject 166 | 167 | Post["/deleteproject"] = _ => 168 | { 169 | _buildConfigurationService.DeleteProject((string)Request.Form["ConnectionId"], (string)Request.Form["ProjectId"]); 170 | 171 | return Response.AsRedirect("/"); 172 | }; 173 | 174 | #endregion 175 | 176 | #region /addbuilds 177 | 178 | Get["/addbuilds"] = x => 179 | { 180 | var connectionId = (string)Request.Query["connectionId"]; 181 | var projectId = (string)Request.Query["projectId"]; 182 | 183 | if (string.IsNullOrEmpty(connectionId) || string.IsNullOrEmpty(projectId)) 184 | { 185 | return new NotFoundResponse(); 186 | } 187 | 188 | try 189 | { 190 | var buildDefinitions =_buildConfigurationService.GetBuildDefinitions(connectionId, projectId, 191 | out var connection, out var project); 192 | 193 | var model = new AddBuildsViewModel 194 | { 195 | Connection = connection, 196 | Project = project, 197 | BuildSelections = buildDefinitions.Select(b => new BuildSelectionModel 198 | { 199 | Id = b.Id, 200 | Name = b.Name, 201 | Disabled = _buildConfigurationService.BuildExistsInConfig(connection.Id, project.Id, b.Id) 202 | 203 | }).ToList(), 204 | }; 205 | 206 | return View["Views/AddBuilds.cshtml", model]; 207 | } 208 | catch (InvalidOperationException) 209 | { 210 | return new NotFoundResponse(); 211 | } 212 | }; 213 | 214 | Post["/addbuilds"] = x => 215 | { 216 | var connectionId = (string)Request.Form["ConnectionId"]; 217 | var projectId = (string)Request.Form["ProjectId"]; 218 | 219 | var selections = this.Bind>(); 220 | 221 | _buildConfigurationService.AddBuildDefinitions(connectionId, projectId, 222 | selections.Where(s => s.Selected).Select(s => new BuildDefinition { Id = s.Id, Name = s.Name }).ToList()); 223 | 224 | return Response.AsRedirect("/"); 225 | }; 226 | 227 | #endregion 228 | 229 | #region /deletebuild 230 | 231 | Post["/deletebuild"] = _ => 232 | { 233 | _buildConfigurationService.DeleteBuild((string)Request.Form["LocalDefId"]); 234 | 235 | return Response.AsRedirect("/"); 236 | }; 237 | 238 | #endregion 239 | } 240 | } 241 | 242 | } 243 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Nancy/Modules/HomeModule.cs: -------------------------------------------------------------------------------- 1 | using Nancy; 2 | using System.Linq; 3 | using Tfs.BuildNotifications.Core.Services.Interfaces; 4 | using Tfs.BuildNotifications.Web.ViewModels; 5 | 6 | namespace Tfs.BuildNotifications.Web.Nancy.Modules 7 | { 8 | public class HomeModule : NancyModule 9 | { 10 | private readonly IBuildConfigurationService _buildConfigurationService; 11 | 12 | public HomeModule(IBuildConfigurationService buildConfigurationService) 13 | { 14 | _buildConfigurationService = buildConfigurationService; 15 | 16 | Get["/"] = _ => 17 | { 18 | if (!_buildConfigurationService.HasConfiguration()) 19 | { 20 | return Response.AsRedirect("/addconnection"); 21 | } 22 | 23 | var buildConfig = _buildConfigurationService.GetBuildConfig(); 24 | var viewModel = new HomeViewModel(); 25 | 26 | foreach (var connection in buildConfig.Connections) 27 | { 28 | var dashboardConnection = new DashboardConnection { Connection = connection }; 29 | 30 | if (!connection.Broken) 31 | { 32 | foreach (var project in connection.Projects) 33 | { 34 | var dashboardProject = new DashboardProject { Project = project }; 35 | 36 | foreach (var buildDef in project.BuildDefinitions) 37 | { 38 | dashboardProject.BuildDefinitions.Add(new DashboardBuildDefinition 39 | { 40 | Name = buildDef.Name, 41 | Url = buildDef.Url, 42 | LocalId = buildDef.LocalId, 43 | 44 | Builds = _buildConfigurationService.GetBuilds(connection, project.Name, 45 | buildDef.Id).ToList() 46 | }); 47 | } 48 | 49 | dashboardConnection.Projects.Add(dashboardProject); 50 | } 51 | } 52 | 53 | viewModel.Connections.Add(dashboardConnection); 54 | } 55 | 56 | return View["Views/Index.cshtml", viewModel]; 57 | }; 58 | 59 | Get["/buildsummary"] = x => 60 | { 61 | var buildConfig = _buildConfigurationService.GetBuildConfig(); 62 | 63 | var connectionId = (string)Request.Query["connectionId"]; 64 | var projectId = (string)Request.Query["projectId"]; 65 | var localBuildId = (string)Request.Query["localBuildId"]; 66 | 67 | var connection = buildConfig.Connections 68 | .FirstOrDefault(c => c.Id.ToString() == connectionId); 69 | 70 | var project = connection?.Projects.FirstOrDefault(p => p.Id.ToString() == projectId); 71 | 72 | var buildDef = project?.BuildDefinitions 73 | .FirstOrDefault(b => b.LocalId.ToString() == localBuildId); 74 | 75 | if (buildDef != null) 76 | { 77 | var model = new SingleBuildDefinitionViewModel 78 | { 79 | Connection = connection, 80 | Project = project, 81 | 82 | BuildDefinition = new DashboardBuildDefinition 83 | { 84 | Name = buildDef.Name, 85 | Url = buildDef.Url, 86 | LocalId = buildDef.LocalId, 87 | 88 | Builds = _buildConfigurationService.GetBuilds(connection, project.Name, 89 | buildDef.Id).ToList() 90 | } 91 | }; 92 | 93 | return View["Views/Shared/_BuildSummary.cshtml", model]; 94 | } 95 | else 96 | { 97 | return null; 98 | } 99 | }; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Nancy/Validators/AddConnectionValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Tfs.BuildNotifications.Web.ViewModels; 3 | 4 | namespace Tfs.BuildNotifications.Web.Nancy.Validators 5 | { 6 | public class AddConnectionValidator : AbstractValidator 7 | { 8 | public AddConnectionValidator() 9 | { 10 | RuleFor(request => request.TfsServerUrl).NotEmpty().WithMessage("Please enter a TFS server/collection URL"); 11 | RuleFor(request => request.TfsServerLocation).NotEmpty().WithMessage("Please select the TFS server location"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Tfs.BuildNotifications.Web")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("Tfs.BuildNotifications.Web")] 12 | [assembly: AssemblyCopyright("Copyright © 2017")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("4db8c252-0575-4436-8595-7247c3a667ca")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Services/Interfaces/IWebsiteDashboardService.cs: -------------------------------------------------------------------------------- 1 | using Tfs.BuildNotifications.Model; 2 | 3 | namespace Tfs.BuildNotifications.Web.Services.Interfaces 4 | { 5 | public interface IWebsiteDashboardService 6 | { 7 | void StartWebsite(); 8 | void UpdateDashboardBuildStatus(Build build); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Services/WebsiteDashboardService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Owin.Hosting; 2 | using System; 3 | using System.Diagnostics; 4 | using Tfs.BuildNotifications.Common.Telemetry.Interfaces; 5 | using Tfs.BuildNotifications.Model; 6 | using Tfs.BuildNotifications.Web.Nancy.Handlers; 7 | using Tfs.BuildNotifications.Web.Services.Interfaces; 8 | using Tfs.BuildNotifications.Web.SignalR.Interfaces; 9 | 10 | namespace Tfs.BuildNotifications.Web.Services 11 | { 12 | public class WebsiteDashboardService : IWebsiteDashboardService 13 | { 14 | public int WebsitePort { get; set; } 15 | 16 | private IDisposable _webApp; 17 | 18 | private readonly IDashboardHub _dashboardHub; 19 | private readonly ILogService _logService; 20 | 21 | public WebsiteDashboardService(IDashboardHub dashboardHub, ILogService logService) 22 | { 23 | _dashboardHub = dashboardHub; 24 | _logService = logService; 25 | 26 | LoggingErrorHandler.LogService = _logService; 27 | } 28 | 29 | public void StartWebsite() 30 | { 31 | var options = new StartOptions(); 32 | 33 | options.Urls.Add($"http://{Environment.MachineName}:{WebsitePort}"); 34 | options.Urls.Add($"http://127.0.0.1:{WebsitePort}"); 35 | options.Urls.Add($"http://localhost:{WebsitePort}"); 36 | 37 | _webApp = WebApp.Start(options); 38 | 39 | #if DEBUG 40 | Process.Start($"http://localhost:{WebsitePort}"); 41 | #endif 42 | _logService.Log($"Dashboard website started on port {WebsitePort}."); 43 | } 44 | 45 | public void UpdateDashboardBuildStatus(Build build) 46 | { 47 | _dashboardHub.NotifyBuildChange(build); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/SignalR/DashboardHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNet.SignalR; 2 | using Newtonsoft.Json; 3 | using System.Linq; 4 | using Tfs.BuildNotifications.Common.Helpers; 5 | using Tfs.BuildNotifications.Core.Clients; 6 | using Tfs.BuildNotifications.Core.Services; 7 | using Tfs.BuildNotifications.Core.Services.Interfaces; 8 | using Tfs.BuildNotifications.Model; 9 | using Tfs.BuildNotifications.Web.Nancy.Handlers; 10 | using Tfs.BuildNotifications.Web.SignalR.Interfaces; 11 | 12 | namespace Tfs.BuildNotifications.Web.SignalR 13 | { 14 | public class DashboardHub : Hub, IDashboardHub 15 | { 16 | private IHubContext _hubContext = GlobalHost.ConnectionManager.GetHubContext(); 17 | 18 | private readonly IBuildConfigurationService _buildConfigurationService; 19 | 20 | public DashboardHub() 21 | { 22 | // ToDo: DI 23 | _buildConfigurationService = new BuildConfigurationService(new TfsApiClient(LoggingErrorHandler.LogService), 24 | new RegistryHelper()); 25 | } 26 | 27 | public void NotifyBuildChange(Build build) 28 | { 29 | _hubContext.Clients.All.buildNotification(JsonConvert.SerializeObject(build)); 30 | } 31 | 32 | public void GetRunningBuilds(string connId) 33 | { 34 | var runningBuilds = _buildConfigurationService.GetLastBuildPerDefinition().Where(b => b.InProgress); 35 | 36 | foreach (var build in runningBuilds) 37 | { 38 | _hubContext.Clients.Client(connId).buildNotification(JsonConvert.SerializeObject(build)); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/SignalR/Interfaces/IDashboardHub.cs: -------------------------------------------------------------------------------- 1 | using Tfs.BuildNotifications.Model; 2 | 3 | namespace Tfs.BuildNotifications.Web.SignalR.Interfaces 4 | { 5 | public interface IDashboardHub 6 | { 7 | void NotifyBuildChange(Build build); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNet.SignalR; 2 | using Microsoft.Owin; 3 | using Microsoft.Owin.Cors; 4 | using Owin; 5 | 6 | [assembly: OwinStartup(typeof(Tfs.BuildNotifications.Web.Startup))] 7 | 8 | namespace Tfs.BuildNotifications.Web 9 | { 10 | public class Startup 11 | { 12 | public void Configuration(IAppBuilder app) 13 | { 14 | app.Map("/signalr", map => 15 | { 16 | map.UseCors(CorsOptions.AllowAll); 17 | 18 | var hubConfiguration = new HubConfiguration 19 | { 20 | EnableDetailedErrors = true, 21 | EnableJSONP = true 22 | }; 23 | 24 | map.RunSignalR(hubConfiguration); 25 | }); 26 | 27 | app.UseNancy(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Tfs.BuildNotifications.Web.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {4DB8C252-0575-4436-8595-7247C3A667CA} 8 | Library 9 | Properties 10 | Tfs.BuildNotifications.Web 11 | Tfs.BuildNotifications.Web 12 | v4.5.2 13 | 512 14 | 15 | 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\packages\FluentValidation.3.4.0.0\lib\Net40\FluentValidation.dll 38 | 39 | 40 | ..\packages\Microsoft.AspNet.SignalR.Core.2.2.2\lib\net45\Microsoft.AspNet.SignalR.Core.dll 41 | 42 | 43 | ..\packages\Microsoft.AspNet.SignalR.SystemWeb.2.2.2\lib\net45\Microsoft.AspNet.SignalR.SystemWeb.dll 44 | 45 | 46 | ..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll 47 | 48 | 49 | ..\packages\Microsoft.Owin.Cors.3.1.0\lib\net45\Microsoft.Owin.Cors.dll 50 | 51 | 52 | ..\packages\Microsoft.Owin.Host.HttpListener.3.1.0\lib\net45\Microsoft.Owin.Host.HttpListener.dll 53 | 54 | 55 | ..\packages\Microsoft.Owin.Host.SystemWeb.2.1.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll 56 | 57 | 58 | ..\packages\Microsoft.Owin.Hosting.3.1.0\lib\net45\Microsoft.Owin.Hosting.dll 59 | 60 | 61 | ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll 62 | 63 | 64 | ..\packages\Nancy.1.4.4\lib\net40\Nancy.dll 65 | 66 | 67 | ..\packages\Nancy.Owin.1.4.1\lib\net40\Nancy.Owin.dll 68 | 69 | 70 | ..\packages\Nancy.Validation.FluentValidation.1.4.1\lib\net40\Nancy.Validation.FluentValidation.dll 71 | 72 | 73 | ..\packages\Nancy.Viewengines.Razor.1.4.3\lib\net40\Nancy.ViewEngines.Razor.dll 74 | 75 | 76 | ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll 77 | 78 | 79 | ..\packages\Owin.1.0\lib\net40\Owin.dll 80 | 81 | 82 | 83 | 84 | ..\packages\Microsoft.AspNet.Cors.5.2.3\lib\net45\System.Web.Cors.dll 85 | 86 | 87 | ..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | Always 122 | 123 | 124 | Always 125 | 126 | 127 | Always 128 | 129 | 130 | Always 131 | 132 | 133 | Always 134 | 135 | 136 | Always 137 | 138 | 139 | Always 140 | 141 | 142 | Always 143 | 144 | 145 | Always 146 | 147 | 148 | Always 149 | 150 | 151 | Always 152 | 153 | 154 | Always 155 | 156 | 157 | Always 158 | 159 | 160 | Always 161 | 162 | 163 | 164 | 165 | Always 166 | 167 | 168 | Always 169 | 170 | 171 | Always 172 | 173 | 174 | Always 175 | 176 | 177 | Always 178 | 179 | 180 | Always 181 | 182 | 183 | Always 184 | 185 | 186 | Always 187 | 188 | 189 | Always 190 | 191 | 192 | Always 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | {f8ef6d0f-a56e-4ba6-b6ba-df9c4a5f6792} 203 | Tfs.BuildNotifications.Common 204 | 205 | 206 | {12d1f80b-4cda-460d-8ab4-5b0bda802f9e} 207 | Tfs.BuildNotifications.Core 208 | 209 | 210 | {fdf1e34f-79bb-4bbf-917d-6e9a6ff64a91} 211 | Tfs.BuildNotifications.Model 212 | 213 | 214 | 215 | 216 | 217 | 218 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/ViewModels/AddBuildsViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Tfs.BuildNotifications.Model; 3 | 4 | namespace Tfs.BuildNotifications.Web.ViewModels 5 | { 6 | public class AddBuildsViewModel : ViewModelBase 7 | { 8 | public Connection Connection { get; set; } 9 | 10 | public Project Project { get; set; } 11 | 12 | public List BuildSelections { get; set; } 13 | } 14 | 15 | public class BuildSelectionModel 16 | { 17 | public string Name { get; set; } 18 | public int Id { get; set; } 19 | public bool Selected { get; set; } 20 | public bool Disabled { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/ViewModels/AddEditConnectionViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Tfs.BuildNotifications.Web.ViewModels 2 | { 3 | public class AddEditConnectionViewModel : ViewModelBase 4 | { 5 | public string TfsServerUrl { get; set; } 6 | public string PersonalAccessToken { get; set; } 7 | public string UserName { get; set; } 8 | public string Password { get; set; } 9 | public string TfsServerLocation { get; set; } 10 | 11 | public bool NewConfiguration { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/ViewModels/AddProjectViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Tfs.BuildNotifications.Model; 4 | 5 | namespace Tfs.BuildNotifications.Web.ViewModels 6 | { 7 | public class AddProjectViewModel : ViewModelBase 8 | { 9 | public Connection Connection { get; set; } 10 | 11 | public List ProjectSelections { get; set; } 12 | } 13 | 14 | public class ProjectSelectionModel 15 | { 16 | public string Name { get; set; } 17 | public Guid Id { get; set; } 18 | public bool Selected { get; set; } 19 | public bool Disabled { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/ViewModels/HomeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Tfs.BuildNotifications.Common.Extensions; 5 | using Tfs.BuildNotifications.Core.Extensions; 6 | using Tfs.BuildNotifications.Model; 7 | 8 | namespace Tfs.BuildNotifications.Web.ViewModels 9 | { 10 | public class HomeViewModel 11 | { 12 | public HomeViewModel() 13 | { 14 | Connections = new List(); 15 | } 16 | 17 | public List Connections { get; set; } 18 | 19 | public string GetHistoryCssClassName(Build build) 20 | { 21 | if (build.InProgress) 22 | { 23 | return "history-running"; 24 | } 25 | 26 | switch (build.GetBuildResult()) 27 | { 28 | case BuildResult.Succeeded: 29 | return "history-success"; 30 | 31 | case BuildResult.Failed: 32 | return "history-fail"; 33 | 34 | default: 35 | return "history-other"; 36 | } 37 | } 38 | 39 | public bool AnyBuildsConfigured => 40 | Connections.Any(c => !c.Connection.Broken && c.Projects.Any(p => p.BuildDefinitions.Any())); 41 | } 42 | 43 | public class DashboardConnection 44 | { 45 | public DashboardConnection() 46 | { 47 | Projects = new List(); 48 | } 49 | 50 | public Connection Connection { get; set; } 51 | 52 | public List Projects { get; set; } 53 | } 54 | 55 | public class DashboardProject 56 | { 57 | public DashboardProject() 58 | { 59 | BuildDefinitions = new List(); 60 | } 61 | 62 | public Project Project { get; set; } 63 | 64 | public List BuildDefinitions { get; set; } 65 | } 66 | 67 | public class DashboardBuildDefinition 68 | { 69 | public DashboardBuildDefinition() 70 | { 71 | Builds = new List(); 72 | } 73 | 74 | public string Name { get; set; } 75 | public string ShortName => Name.Shorten(40, "..."); 76 | 77 | public List Builds { get; set; } 78 | 79 | public bool IsRunning => Builds.Any(b => b.InProgress); 80 | 81 | public string LastRequestedBy => Builds.FirstOrDefault()?.LastRequestedBy; 82 | public string LastFinished => Builds.FirstOrDefault()?.LastFinished?.ToString() ?? "N/A"; 83 | public string LastStarted => Builds.FirstOrDefault()?.StartTime?.ToString() ?? "Unknown"; 84 | public string Url { get; set; } 85 | 86 | public Guid LocalId { get; set; } 87 | 88 | public BuildResult Status => Builds.FirstOrDefault()?.GetBuildResult() ?? BuildResult.Unknown; 89 | 90 | public bool RequiresAttention => Status == BuildResult.Failed || Status == BuildResult.Stopped; 91 | 92 | public int LastBuildId => Builds?.FirstOrDefault()?.BuildRunId ?? 0; 93 | 94 | public string StatusImageFileName 95 | { 96 | get 97 | { 98 | if (IsRunning) 99 | { 100 | return "play.png"; 101 | } 102 | 103 | switch (Status) 104 | { 105 | case BuildResult.Succeeded: 106 | return "success.png"; 107 | 108 | case BuildResult.Failed: 109 | return "failed.png"; 110 | 111 | case BuildResult.Stopped: 112 | return "stopped.png"; 113 | 114 | default: 115 | return "unknown.png"; 116 | } 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/ViewModels/SingleBuildDefinitionViewModel.cs: -------------------------------------------------------------------------------- 1 | using Tfs.BuildNotifications.Model; 2 | 3 | namespace Tfs.BuildNotifications.Web.ViewModels 4 | { 5 | public class SingleBuildDefinitionViewModel : HomeViewModel 6 | { 7 | public Connection Connection { get; set; } 8 | 9 | public Project Project { get; set; } 10 | 11 | public DashboardBuildDefinition BuildDefinition { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Tfs.BuildNotifications.Web.ViewModels 4 | { 5 | public class ViewModelBase 6 | { 7 | public ViewModelBase() 8 | { 9 | Errors = new List(); 10 | } 11 | 12 | public List Errors { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Views/AddBuilds.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase 2 | 3 | @{ 4 | ViewBag.Title = "Add Builds"; 5 | Layout = "Views/Shared/_Layout.cshtml"; 6 | } 7 | 8 |

Go back

9 | 10 |

Add Builds

11 | 12 |

@Model.Project.Name (@Model.Connection.TfsServerUrl)

13 | 14 | @if (!Model.BuildSelections.Any()) 15 | { 16 |
17 |

No build definitions found (or visible) for the current project.

18 |
19 | } 20 | else 21 | { 22 | var count = 0; 23 | 24 |

Select one or more builds to enable notifications for.

25 | 26 |
27 |

28 | Select all 29 |   |   30 | Deselect all 31 |

32 |
33 | 34 |
35 | 36 | 37 | 38 | 39 |
40 | @foreach (var item in Model.BuildSelections) 41 | { 42 |
43 | 44 | 45 | disabled="disabled" } /> 46 | 47 | 54 |
55 | 56 | count++; 57 | } 58 |
59 | 60 | 61 |
62 | 63 | 76 | } -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Views/AddConnection.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase 2 | 3 | @{ 4 | ViewBag.Title = "Add a Project"; 5 | Layout = "Views/Shared/_Layout.cshtml"; 6 | } 7 | 8 |

Go back

9 | 10 |

Add a Connection

11 | 12 | @if (Model.NewConfiguration) 13 | { 14 |
15 |

16 | You don't have any TFS projects/servers configured to monitor builds. Get started by adding a TFS connection 17 | below. 18 |

19 |
20 | } 21 | 22 | @Html.Partial("Shared/_AddEditConnection", Model) -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Views/AddProject.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase 2 | 3 | @{ 4 | ViewBag.Title = "Add Projects"; 5 | Layout = "Views/Shared/_Layout.cshtml"; 6 | } 7 | 8 |

Go back

9 | 10 |

Add Projects

11 | 12 |

@Model.Connection.TfsServerUrl (@Model.Connection.TfsServerDeployment.ToFriendlyName())

13 | 14 | @if (!Model.ProjectSelections.Any()) 15 | { 16 |
17 |

No projects found (or visible) for the current connection.

18 |
19 | 20 |

Go back

21 | } 22 | else 23 | { 24 | var count = 0; 25 | 26 |

Select one or more Team Projects to set up build notifications for.

27 | 28 |
29 | 30 | 31 | 32 |
33 | @foreach (var item in Model.ProjectSelections) 34 | { 35 |
36 | 37 | 38 | disabled="disabled" } /> 39 | 40 | 47 |
48 | 49 | count++; 50 | } 51 |
52 | 53 | 54 |
55 | } -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Views/Configure.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase 2 | 3 | @{ 4 | ViewBag.Title = "Configure"; 5 | Layout = "Views/Shared/_Layout.cshtml"; 6 | } -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Views/EditConnection.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase 2 | 3 | @{ 4 | ViewBag.Title = "Add a Project"; 5 | Layout = "Views/Shared/_Layout.cshtml"; 6 | } 7 | 8 |

Go back

9 | 10 |

Edit Connection

11 | 12 | @Html.Partial("Shared/_AddEditConnection", Model) 13 | 14 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Views/Error.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase 2 | 3 | @{ 4 | ViewBag.Title = "Error"; 5 | Layout = "Views/Shared/_Layout.cshtml"; 6 | } 7 | 8 |

Sorry, something went wrong...

9 | 10 |

11 | Oops, an error seems to have occurred. Sorry about that. 12 |

13 | 14 | @if (Model != null) 15 | { 16 |

Error detail:

17 |

@Model.ToString()

18 | } 19 | 20 |

Please try again or go back.

-------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Views/Index.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase 2 | 3 | @{ 4 | ViewBag.Title = "Home"; 5 | Layout = "Views/Shared/_Layout.cshtml"; 6 | } 7 | 8 | 16 | 17 | @if (!Model.Connections.Any()) 18 | { 19 |

Add a Connection

20 | 21 |
22 |

23 | You don't have any TFS projects/servers configured to monitor builds. Get started by 24 | adding a TFS connection 25 |

26 |
27 | } 28 | else 29 | { 30 | 31 | 32 | 33 | 34 |

Dashboard

35 | 36 | 39 | 40 | if (Model.AnyBuildsConfigured) 41 | { 42 |
43 |

Running Now

44 |

There are no builds currently running.

45 |
46 | } 47 | 48 |
49 | 50 | 51 | foreach (var dashboardConnection in Model.Connections) 52 | { 53 |
54 |

55 | 56 | @dashboardConnection.Connection.TfsServerUrl (@dashboardConnection.Connection.TfsServerDeployment.ToFriendlyName()) 57 | 58 | 59 | 60 | 61 | 62 |

63 |
64 | 65 | @if (dashboardConnection.Connection.Broken) 66 | { 67 |
68 |

69 | An error occurred whilst trying to connect to this TFS instance. Error detail: 70 | '@dashboardConnection.Connection.LastConnectionError' 71 |

72 | 73 |

Click here to edit connection details.

74 | 75 |
76 | 77 | 80 | } 81 | else 82 | { 83 | 90 | 91 | if (!dashboardConnection.Projects.Any()) 92 | { 93 |
94 |

95 | You don't have any TFS projects configured to monitor builds for this connection. 96 | Click here to add a project. 97 |

98 |
99 | } 100 | 101 | foreach (var dashboardProject in dashboardConnection.Projects.OrderBy(p => p.Project.Name)) 102 | { 103 |

@dashboardProject.Project.Name (monitoring @dashboardProject.BuildDefinitions.Count build @("definition".Pluralize(dashboardProject.BuildDefinitions.Count)))

104 | 105 | 110 | 111 | if (!dashboardProject.BuildDefinitions.Any()) 112 | { 113 |
114 |

115 | You are not monitoring any builds for this project. 116 | 117 | Click here to configure build notifications 118 | . 119 |

120 |
121 | } 122 | else 123 | { 124 | var i = 0; 125 | 126 | foreach (var buildDefinition in dashboardProject.BuildDefinitions.OrderByDescending(b => b.RequiresAttention).ThenBy(b => b.Name)) 127 | { 128 |
132 |

133 | 134 | @buildDefinition.ShortName 135 |

136 | 137 | 138 | 139 | 140 | 141 |
142 | @if (!buildDefinition.Builds.Any()) 143 | { 144 |

No builds available for this definition.

145 | } 146 | else 147 | { 148 | if (buildDefinition.IsRunning) 149 | { 150 |

Requested by: @buildDefinition.LastRequestedBy

151 |

Started at: @buildDefinition.LastStarted

152 | } 153 | else 154 | { 155 |

Last requested by: @buildDefinition.LastRequestedBy

156 |

Last completed: @buildDefinition.LastFinished

157 | } 158 | 159 |
160 | @foreach (var build in buildDefinition.Builds.Take(6)) 161 | { 162 | 163 | 164 | 165 | } 166 |
167 | } 168 |
169 |
170 | 171 | i++; 172 | } 173 | 174 |
175 | } 176 | 177 | } 178 | } 179 |
180 |
181 | } 182 | 183 | if (Model.AnyBuildsConfigured) 184 | { 185 | 192 | } 193 | 194 | 265 | } -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Views/NotFound.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase 2 | 3 | @{ 4 | ViewBag.Title = "Page Not Found"; 5 | Layout = "Views/Shared/_Layout.cshtml"; 6 | } 7 | 8 |

Page Not Found

9 | 10 |

11 | Sorry, we couldn't find the the page you were looking for. Please try again or 12 | go back. 13 |

-------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Views/Shared/_AddEditConnection.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase 2 | 3 |
4 | 5 | @if (Model.Errors.Any()) 6 | { 7 |
8 | 9 | @foreach (string error in Model.Errors) 10 | { 11 |

@error

12 | } 13 |
14 | } 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 |
23 |

Example - On premises: 'http://mytfsserver:8080/tfs/defaultcollection'.

24 |

Example - Visual Studio Online: 'https://myname.visualstudio.com/defaultcollection'.

25 |
26 |
27 | 28 |
29 | 30 |
31 | checked="checked" } /> 32 | 33 | 34 | 35 | checked="checked" } /> 36 | 37 | 38 |
39 |
40 | 41 |
42 | 43 |
44 | 45 | 46 | 47 |
48 |

Include domain name e.g. mydomain\username

49 |
50 | 51 |
52 | 53 |
54 | 55 | 56 |
57 | 58 |
59 | 60 | 61 | 62 | 65 |
66 | 67 | 68 |
69 |
70 | 71 | 103 | 104 | @if (Model.Errors.Any()) 105 | { 106 | 109 | } 110 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Views/Shared/_BuildSummary.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase 2 | 3 |
7 |

8 | 9 | @Model.BuildDefinition.ShortName 10 |

11 | 12 | 13 | 14 | 15 | 16 |
17 | @if (!Model.BuildDefinition.Builds.Any()) 18 | { 19 |

No builds available for this definition.

20 | } 21 | else 22 | { 23 | if (Model.BuildDefinition.IsRunning) 24 | { 25 |

Requested by: @Model.BuildDefinition.LastRequestedBy

26 |

Started at: @Model.BuildDefinition.LastStarted

27 | } 28 | else 29 | { 30 |

Last requested by: @Model.BuildDefinition.LastRequestedBy

31 |

Last completed: @Model.BuildDefinition.LastFinished

32 | } 33 | 34 |
35 | @foreach (var build in Model.BuildDefinition.Builds.Take(6)) 36 | { 37 | 38 | 39 | 40 | } 41 |
42 | } 43 |
44 |
-------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Nancy.ViewEngines.Razor.NancyRazorViewBase 2 | 3 | 4 | 5 | 6 | 7 | 8 | TFS Build Notifications - @ViewBag.Title 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 |
20 | 21 |

Team Foundation Server Build Notifications

22 |
23 | 24 |
25 |
26 | @RenderBody() 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/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 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.Web/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 | -------------------------------------------------------------------------------- /Tfs.BuildNotifications/Tfs.BuildNotifications.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tfs.BuildNotifications.Core", "Tfs.BuildNotifications.Core\Tfs.BuildNotifications.Core.csproj", "{12D1F80B-4CDA-460D-8AB4-5B0BDA802F9E}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tfs.BuildNotifications.Model", "Tfs.BuildNotifications.Model\Tfs.BuildNotifications.Model.csproj", "{FDF1E34F-79BB-4BBF-917D-6E9A6FF64A91}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tfs.BuildNotifications.Common", "Tfs.BuildNotifications.Common\Tfs.BuildNotifications.Common.csproj", "{F8EF6D0F-A56E-4BA6-B6BA-DF9C4A5F6792}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tfs.BuildNotifications.Tray", "Tfs.BuildNotifications.Tray\Tfs.BuildNotifications.Tray.csproj", "{D121EF9F-AC72-49AE-8BB0-31F0924BFFF5}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tfs.BuildNotifications.Web", "Tfs.BuildNotifications.Web\Tfs.BuildNotifications.Web.csproj", "{4DB8C252-0575-4436-8595-7247C3A667CA}" 15 | EndProject 16 | Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "Tfs.BuildNotifications.Setup", "Tfs.BuildNotifications.Setup\Tfs.BuildNotifications.Setup.wixproj", "{91521B2E-84C7-43AD-A951-5D631F438B2E}" 17 | ProjectSection(ProjectDependencies) = postProject 18 | {4DB8C252-0575-4436-8595-7247C3A667CA} = {4DB8C252-0575-4436-8595-7247C3A667CA} 19 | {D121EF9F-AC72-49AE-8BB0-31F0924BFFF5} = {D121EF9F-AC72-49AE-8BB0-31F0924BFFF5} 20 | EndProjectSection 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Debug|x86 = Debug|x86 26 | Release|Any CPU = Release|Any CPU 27 | Release|x86 = Release|x86 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {12D1F80B-4CDA-460D-8AB4-5B0BDA802F9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {12D1F80B-4CDA-460D-8AB4-5B0BDA802F9E}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {12D1F80B-4CDA-460D-8AB4-5B0BDA802F9E}.Debug|x86.ActiveCfg = Debug|Any CPU 33 | {12D1F80B-4CDA-460D-8AB4-5B0BDA802F9E}.Debug|x86.Build.0 = Debug|Any CPU 34 | {12D1F80B-4CDA-460D-8AB4-5B0BDA802F9E}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {12D1F80B-4CDA-460D-8AB4-5B0BDA802F9E}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {12D1F80B-4CDA-460D-8AB4-5B0BDA802F9E}.Release|x86.ActiveCfg = Release|Any CPU 37 | {12D1F80B-4CDA-460D-8AB4-5B0BDA802F9E}.Release|x86.Build.0 = Release|Any CPU 38 | {FDF1E34F-79BB-4BBF-917D-6E9A6FF64A91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {FDF1E34F-79BB-4BBF-917D-6E9A6FF64A91}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {FDF1E34F-79BB-4BBF-917D-6E9A6FF64A91}.Debug|x86.ActiveCfg = Debug|Any CPU 41 | {FDF1E34F-79BB-4BBF-917D-6E9A6FF64A91}.Debug|x86.Build.0 = Debug|Any CPU 42 | {FDF1E34F-79BB-4BBF-917D-6E9A6FF64A91}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {FDF1E34F-79BB-4BBF-917D-6E9A6FF64A91}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {FDF1E34F-79BB-4BBF-917D-6E9A6FF64A91}.Release|x86.ActiveCfg = Release|Any CPU 45 | {FDF1E34F-79BB-4BBF-917D-6E9A6FF64A91}.Release|x86.Build.0 = Release|Any CPU 46 | {F8EF6D0F-A56E-4BA6-B6BA-DF9C4A5F6792}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {F8EF6D0F-A56E-4BA6-B6BA-DF9C4A5F6792}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {F8EF6D0F-A56E-4BA6-B6BA-DF9C4A5F6792}.Debug|x86.ActiveCfg = Debug|Any CPU 49 | {F8EF6D0F-A56E-4BA6-B6BA-DF9C4A5F6792}.Debug|x86.Build.0 = Debug|Any CPU 50 | {F8EF6D0F-A56E-4BA6-B6BA-DF9C4A5F6792}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {F8EF6D0F-A56E-4BA6-B6BA-DF9C4A5F6792}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {F8EF6D0F-A56E-4BA6-B6BA-DF9C4A5F6792}.Release|x86.ActiveCfg = Release|Any CPU 53 | {F8EF6D0F-A56E-4BA6-B6BA-DF9C4A5F6792}.Release|x86.Build.0 = Release|Any CPU 54 | {D121EF9F-AC72-49AE-8BB0-31F0924BFFF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {D121EF9F-AC72-49AE-8BB0-31F0924BFFF5}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {D121EF9F-AC72-49AE-8BB0-31F0924BFFF5}.Debug|x86.ActiveCfg = Debug|Any CPU 57 | {D121EF9F-AC72-49AE-8BB0-31F0924BFFF5}.Debug|x86.Build.0 = Debug|Any CPU 58 | {D121EF9F-AC72-49AE-8BB0-31F0924BFFF5}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {D121EF9F-AC72-49AE-8BB0-31F0924BFFF5}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {D121EF9F-AC72-49AE-8BB0-31F0924BFFF5}.Release|x86.ActiveCfg = Release|Any CPU 61 | {D121EF9F-AC72-49AE-8BB0-31F0924BFFF5}.Release|x86.Build.0 = Release|Any CPU 62 | {4DB8C252-0575-4436-8595-7247C3A667CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {4DB8C252-0575-4436-8595-7247C3A667CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {4DB8C252-0575-4436-8595-7247C3A667CA}.Debug|x86.ActiveCfg = Debug|Any CPU 65 | {4DB8C252-0575-4436-8595-7247C3A667CA}.Debug|x86.Build.0 = Debug|Any CPU 66 | {4DB8C252-0575-4436-8595-7247C3A667CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {4DB8C252-0575-4436-8595-7247C3A667CA}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {4DB8C252-0575-4436-8595-7247C3A667CA}.Release|x86.ActiveCfg = Release|Any CPU 69 | {4DB8C252-0575-4436-8595-7247C3A667CA}.Release|x86.Build.0 = Release|Any CPU 70 | {91521B2E-84C7-43AD-A951-5D631F438B2E}.Debug|Any CPU.ActiveCfg = Debug|x86 71 | {91521B2E-84C7-43AD-A951-5D631F438B2E}.Debug|Any CPU.Build.0 = Debug|x86 72 | {91521B2E-84C7-43AD-A951-5D631F438B2E}.Debug|x86.ActiveCfg = Debug|x86 73 | {91521B2E-84C7-43AD-A951-5D631F438B2E}.Debug|x86.Build.0 = Debug|x86 74 | {91521B2E-84C7-43AD-A951-5D631F438B2E}.Release|Any CPU.ActiveCfg = Release|x86 75 | {91521B2E-84C7-43AD-A951-5D631F438B2E}.Release|Any CPU.Build.0 = Release|x86 76 | {91521B2E-84C7-43AD-A951-5D631F438B2E}.Release|x86.ActiveCfg = Release|x86 77 | {91521B2E-84C7-43AD-A951-5D631F438B2E}.Release|x86.Build.0 = Release|x86 78 | EndGlobalSection 79 | GlobalSection(SolutionProperties) = preSolution 80 | HideSolutionNode = FALSE 81 | EndGlobalSection 82 | GlobalSection(ExtensibilityGlobals) = postSolution 83 | SolutionGuid = {9CC530B7-F089-497B-9AFB-E07991C4872A} 84 | EndGlobalSection 85 | EndGlobal 86 | -------------------------------------------------------------------------------- /docs/images/build-failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/docs/images/build-failed.png -------------------------------------------------------------------------------- /docs/images/build-passed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/docs/images/build-passed.png -------------------------------------------------------------------------------- /docs/images/build-started.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/docs/images/build-started.png -------------------------------------------------------------------------------- /docs/images/dashboard-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/docs/images/dashboard-example.png -------------------------------------------------------------------------------- /downloads/tfs-build-notifications-setup.msi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattwendels/tfs-build-notifications/73bdeb3a6f28e884d0bb75379f660893c0d2961b/downloads/tfs-build-notifications-setup.msi --------------------------------------------------------------------------------