├── .all-contributorsrc ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── feature_request.md └── workflows │ ├── _build.yaml │ ├── pr.yaml │ └── publish.yaml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── src ├── .gitattributes ├── Commands │ ├── ActionsToolWindowCommand.cs │ ├── GotoRepoCommand.cs │ ├── OpenSettingsCommand.cs │ ├── RefreshRepoCommand.cs │ └── ReportFeedbackCommand.cs ├── Converters │ ├── ConclusionColorConverter.cs │ ├── ConclusionIconConverter.cs │ └── NullToVisibilityConverter.cs ├── GitHubActionsVS.csproj ├── GitHubActionsVS.sln ├── GitHubActionsVSPackage.cs ├── Helpers │ ├── ConclusionFilter.cs │ ├── CredentialManager.cs │ └── RepoInfo.cs ├── LICENSE ├── Models │ ├── BaseWorkflowType.cs │ ├── SimpleEnvironment.cs │ ├── SimpleJob.cs │ └── SimpleRun.cs ├── Options │ └── ExtensionOptions.cs ├── Properties │ └── AssemblyInfo.cs ├── Resources │ ├── AddItem.png │ ├── CancelBuild.png │ ├── Delete.png │ ├── Edit.png │ ├── GitHub.png │ ├── Icon.png │ ├── OpenWebSite.png │ ├── Run.png │ ├── UIStrings.Designer.cs │ ├── UIStrings.resx │ └── codicon.ttf ├── ToolWindows │ ├── ActionsToolWindow.cs │ ├── GHActionsToolWindow.xaml │ ├── GHActionsToolWindow.xaml.cs │ ├── MessageCommand.cs │ ├── MessagePayload.cs │ └── ToolWindowMessenger.cs ├── UserControls │ ├── AddEditSecret.xaml │ └── AddEditSecret.xaml.cs ├── VSCommandTable.cs ├── VSCommandTable.vsct ├── lib │ └── win32 │ │ └── x64 │ │ └── git2-e632535.dll ├── libsodium.dll ├── source.extension.cs └── source.extension.vsixmanifest ├── version.json └── vs-publish.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "IEvangelist", 12 | "name": "David Pine", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/7679720?v=4", 14 | "profile": "https://davidpine.net", 15 | "contributions": [ 16 | "code", 17 | "doc" 18 | ] 19 | }, 20 | { 21 | "login": "timheuer", 22 | "name": "Tim Heuer", 23 | "avatar_url": "https://avatars.githubusercontent.com/u/4821?v=4", 24 | "profile": "https://timheuer.com/blog/", 25 | "contributions": [ 26 | "code", 27 | "doc" 28 | ] 29 | }, 30 | { 31 | "login": "zlatanov", 32 | "name": "Ivan Zlatanov", 33 | "avatar_url": "https://avatars.githubusercontent.com/u/2470527?v=4", 34 | "profile": "https://github.com/zlatanov", 35 | "contributions": [ 36 | "code" 37 | ] 38 | } 39 | ], 40 | "contributorsPerLine": 7, 41 | "skipCi": true, 42 | "repoType": "github", 43 | "repoHost": "https://github.com", 44 | "projectName": "GitHubActionsVS", 45 | "projectOwner": "timheuer" 46 | } 47 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [timheuer] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[BUG]: " 4 | labels: ["bug"] 5 | assignees: 6 | - timheuer 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this bug report! 12 | - type: textarea 13 | id: what-happened 14 | attributes: 15 | label: What happened? 16 | description: Also tell us, what did you expect to happen? 17 | placeholder: Tell us what you see! 18 | value: "A bug happened!" 19 | validations: 20 | required: true 21 | - type: input 22 | id: vsversion 23 | attributes: 24 | label: Visual Studio Version 25 | description: Copy the Visual Studio version from your Help...About Visual Studio menu 26 | placeholder: Version 17.8.5 27 | - type: textarea 28 | id: logs 29 | attributes: 30 | label: Relevant log output 31 | description: Please copy and paste any relevant log output. From the Output window for "GitHub Actions for VS" 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/_build.yaml: -------------------------------------------------------------------------------- 1 | name: "Base build" 2 | 3 | on: 4 | workflow_call: 5 | outputs: 6 | version: 7 | description: 'Version of the build' 8 | value: ${{ jobs.build.outputs.version }} 9 | workflow_dispatch: 10 | inputs: 11 | Reason: 12 | description: 'Reason for the build' 13 | 14 | jobs: 15 | build: 16 | outputs: 17 | version: ${{ steps.vsix_version.outputs.SimpleVersion }} 18 | name: Build 19 | runs-on: windows-2022 20 | env: 21 | PROJECT_PATH: "src/GitHubActionsVS.sln" 22 | VsixManifestPath: src\source.extension.vsixmanifest 23 | VsixManifestSourcePath: src\source.extension.cs 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Version stamping 31 | id: vsix_version 32 | uses: dotnet/nbgv@v0.4 33 | with: 34 | setAllVars: true 35 | 36 | - name: 🧰 Setup .NET build dependencies 37 | uses: timheuer/bootstrap-dotnet@v1 38 | with: 39 | nuget: 'false' 40 | sdk: 'false' 41 | msbuild: 'true' 42 | 43 | - name: Increment VSIX version 44 | id: vsix_version_stamp 45 | uses: timheuer/vsix-version-stamp@v2 46 | with: 47 | manifest-file: ${{ env.VsixManifestPath }} 48 | vsix-token-source-file: ${{ env.VsixManifestSourcePath }} 49 | version-number: ${{ steps.vsix_version.outputs.SimpleVersion }} 50 | 51 | - name: 🏗️ Build 52 | run: msbuild ${{ env.PROJECT_PATH }} /p:Configuration=Release /v:m -restore /p:OutDir=\_built -bl 53 | 54 | - name: ⬆️ Upload artifact 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: msbuild.binlog 58 | path: msbuild.binlog 59 | 60 | - name: ⬆️ Upload artifact 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: ${{ github.event.repository.name }}.vsix 64 | path: /_built/**/*.vsix 65 | 66 | - name: Echo version 67 | run: | 68 | Write-Output ${{ steps.vsix_version.outputs.SimpleVersion }} -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: "Build PR" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**/*.md' 9 | - '**/*.gitignore' 10 | - '**/*.gitattributes' 11 | 12 | jobs: 13 | build: 14 | name: Build and Test 15 | uses: ./.github/workflows/_build.yaml -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: "Publish" 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | name: Build and Test 8 | uses: ./.github/workflows/_build.yaml 9 | 10 | publish: 11 | needs: build 12 | environment: 13 | name: production 14 | url: https://marketplace.visualstudio.com/items?itemName=TimHeuer.GitHubActionsVS 15 | name: Publish 16 | runs-on: windows-2022 17 | permissions: 18 | contents: write 19 | 20 | env: 21 | VERSION: ${{ needs.build.outputs.version }} 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Download Package artifact 29 | uses: actions/download-artifact@v4 30 | with: 31 | name: ${{ github.event.repository.name }}.vsix 32 | 33 | - name: Tag and Release 34 | id: tag_release 35 | uses: softprops/action-gh-release@v1 36 | with: 37 | body: Release ${{ env.VERSION }} 38 | tag_name: ${{ env.VERSION }} 39 | generate_release_notes: true 40 | files: | 41 | **/*.vsix 42 | 43 | - name: Upload to VsixGallery 44 | uses: timheuer/openvsixpublish@v1 45 | with: 46 | vsix-file: ${{ github.event.repository.name }}.vsix 47 | 48 | - name: Publish extension to Marketplace 49 | #if: ${{ contains(github.event.head_commit.message, '[release]') }} 50 | continue-on-error: true # remove after VS bug fix 51 | uses: cezarypiatek/VsixPublisherAction@1.1 52 | with: 53 | extension-file: '${{ github.event.repository.name }}.vsix' 54 | publish-manifest-file: 'vs-publish.json' 55 | personal-access-code: ${{ secrets.VS_PUBLISHER_ACCESS_TOKEN }} 56 | 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | [Aa][Rr][Mm]/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Oo]ut/ 29 | [Ll]og/ 30 | [Ll]ogs/ 31 | 32 | # Visual Studio 2015/2017 cache/options directory 33 | .vs/ 34 | # Uncomment if you have tasks that create the project's static files in wwwroot 35 | #wwwroot/ 36 | 37 | # Visual Studio 2017 auto generated files 38 | Generated\ Files/ 39 | 40 | # MSTest test Results 41 | [Tt]est[Rr]esult*/ 42 | [Bb]uild[Ll]og.* 43 | 44 | # NUnit 45 | *.VisualState.xml 46 | TestResult.xml 47 | nunit-*.xml 48 | 49 | # Build Results of an ATL Project 50 | [Dd]ebugPS/ 51 | [Rr]eleasePS/ 52 | dlldata.c 53 | 54 | # Benchmark Results 55 | BenchmarkDotNet.Artifacts/ 56 | 57 | # .NET Core 58 | project.lock.json 59 | project.fragment.lock.json 60 | artifacts/ 61 | 62 | # ASP.NET Scaffolding 63 | ScaffoldingReadMe.txt 64 | 65 | # StyleCop 66 | StyleCopReport.xml 67 | 68 | # Files built by Visual Studio 69 | *_i.c 70 | *_p.c 71 | *_h.h 72 | *.ilk 73 | *.meta 74 | *.obj 75 | *.iobj 76 | *.pch 77 | *.pdb 78 | *.ipdb 79 | *.pgc 80 | *.pgd 81 | *.rsp 82 | *.sbr 83 | *.tlb 84 | *.tli 85 | *.tlh 86 | *.tmp 87 | *.tmp_proj 88 | *_wpftmp.csproj 89 | *.log 90 | *.vspscc 91 | *.vssscc 92 | .builds 93 | *.pidb 94 | *.svclog 95 | *.scc 96 | 97 | # Chutzpah Test files 98 | _Chutzpah* 99 | 100 | # Visual C++ cache files 101 | ipch/ 102 | *.aps 103 | *.ncb 104 | *.opendb 105 | *.opensdf 106 | *.sdf 107 | *.cachefile 108 | *.VC.db 109 | *.VC.VC.opendb 110 | 111 | # Visual Studio profiler 112 | *.psess 113 | *.vsp 114 | *.vspx 115 | *.sap 116 | 117 | # Visual Studio Trace Files 118 | *.e2e 119 | 120 | # TFS 2012 Local Workspace 121 | $tf/ 122 | 123 | # Guidance Automation Toolkit 124 | *.gpState 125 | 126 | # ReSharper is a .NET coding add-in 127 | _ReSharper*/ 128 | *.[Rr]e[Ss]harper 129 | *.DotSettings.user 130 | 131 | # TeamCity is a build add-in 132 | _TeamCity* 133 | 134 | # DotCover is a Code Coverage Tool 135 | *.dotCover 136 | 137 | # AxoCover is a Code Coverage Tool 138 | .axoCover/* 139 | !.axoCover/settings.json 140 | 141 | # Coverlet is a free, cross platform Code Coverage Tool 142 | coverage*.json 143 | coverage*.xml 144 | coverage*.info 145 | 146 | # Visual Studio code coverage results 147 | *.coverage 148 | *.coveragexml 149 | 150 | # NCrunch 151 | _NCrunch_* 152 | .*crunch*.local.xml 153 | nCrunchTemp_* 154 | 155 | # MightyMoose 156 | *.mm.* 157 | AutoTest.Net/ 158 | 159 | # Web workbench (sass) 160 | .sass-cache/ 161 | 162 | # Installshield output folder 163 | [Ee]xpress/ 164 | 165 | # DocProject is a documentation generator add-in 166 | DocProject/buildhelp/ 167 | DocProject/Help/*.HxT 168 | DocProject/Help/*.HxC 169 | DocProject/Help/*.hhc 170 | DocProject/Help/*.hhk 171 | DocProject/Help/*.hhp 172 | DocProject/Help/Html2 173 | DocProject/Help/html 174 | 175 | # Click-Once directory 176 | publish/ 177 | 178 | # Publish Web Output 179 | *.[Pp]ublish.xml 180 | *.azurePubxml 181 | # Note: Comment the next line if you want to checkin your web deploy settings, 182 | # but database connection strings (with potential passwords) will be unencrypted 183 | *.pubxml 184 | *.publishproj 185 | 186 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 187 | # checkin your Azure Web App publish settings, but sensitive information contained 188 | # in these scripts will be unencrypted 189 | PublishScripts/ 190 | 191 | # NuGet Packages 192 | *.nupkg 193 | # NuGet Symbol Packages 194 | *.snupkg 195 | # The packages folder can be ignored because of Package Restore 196 | **/[Pp]ackages/* 197 | # except build/, which is used as an MSBuild target. 198 | !**/[Pp]ackages/build/ 199 | # Uncomment if necessary however generally it will be regenerated when needed 200 | #!**/[Pp]ackages/repositories.config 201 | # NuGet v3's project.json files produces more ignorable files 202 | *.nuget.props 203 | *.nuget.targets 204 | 205 | # Microsoft Azure Build Output 206 | csx/ 207 | *.build.csdef 208 | 209 | # Microsoft Azure Emulator 210 | ecf/ 211 | rcf/ 212 | 213 | # Windows Store app package directories and files 214 | AppPackages/ 215 | BundleArtifacts/ 216 | Package.StoreAssociation.xml 217 | _pkginfo.txt 218 | *.appx 219 | *.appxbundle 220 | *.appxupload 221 | 222 | # Visual Studio cache files 223 | # files ending in .cache can be ignored 224 | *.[Cc]ache 225 | # but keep track of directories ending in .cache 226 | !?*.[Cc]ache/ 227 | 228 | # Others 229 | ClientBin/ 230 | ~$* 231 | *~ 232 | *.dbmdl 233 | *.dbproj.schemaview 234 | *.jfm 235 | *.pfx 236 | *.publishsettings 237 | orleans.codegen.cs 238 | 239 | # Including strong name files can present a security risk 240 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 241 | #*.snk 242 | 243 | # Since there are multiple workflows, uncomment next line to ignore bower_components 244 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 245 | #bower_components/ 246 | 247 | # RIA/Silverlight projects 248 | Generated_Code/ 249 | 250 | # Backup & report files from converting an old project file 251 | # to a newer Visual Studio version. Backup files are not needed, 252 | # because we have git ;-) 253 | _UpgradeReport_Files/ 254 | Backup*/ 255 | UpgradeLog*.XML 256 | UpgradeLog*.htm 257 | ServiceFabricBackup/ 258 | *.rptproj.bak 259 | 260 | # SQL Server files 261 | *.mdf 262 | *.ldf 263 | *.ndf 264 | 265 | # Business Intelligence projects 266 | *.rdl.data 267 | *.bim.layout 268 | *.bim_*.settings 269 | *.rptproj.rsuser 270 | *- [Bb]ackup.rdl 271 | *- [Bb]ackup ([0-9]).rdl 272 | *- [Bb]ackup ([0-9][0-9]).rdl 273 | 274 | # Microsoft Fakes 275 | FakesAssemblies/ 276 | 277 | # GhostDoc plugin setting file 278 | *.GhostDoc.xml 279 | 280 | # Node.js Tools for Visual Studio 281 | .ntvs_analysis.dat 282 | node_modules/ 283 | 284 | # Visual Studio 6 build log 285 | *.plg 286 | 287 | # Visual Studio 6 workspace options file 288 | *.opt 289 | 290 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 291 | *.vbw 292 | 293 | # Visual Studio LightSwitch build output 294 | **/*.HTMLClient/GeneratedArtifacts 295 | **/*.DesktopClient/GeneratedArtifacts 296 | **/*.DesktopClient/ModelManifest.xml 297 | **/*.Server/GeneratedArtifacts 298 | **/*.Server/ModelManifest.xml 299 | _Pvt_Extensions 300 | 301 | # Paket dependency manager 302 | .paket/paket.exe 303 | paket-files/ 304 | 305 | # FAKE - F# Make 306 | .fake/ 307 | 308 | # CodeRush personal settings 309 | .cr/personal 310 | 311 | # Python Tools for Visual Studio (PTVS) 312 | __pycache__/ 313 | *.pyc 314 | 315 | # Cake - Uncomment if you are using it 316 | # tools/** 317 | # !tools/packages.config 318 | 319 | # Tabs Studio 320 | *.tss 321 | 322 | # Telerik's JustMock configuration file 323 | *.jmconfig 324 | 325 | # BizTalk build output 326 | *.btp.cs 327 | *.btm.cs 328 | *.odx.cs 329 | *.xsd.cs 330 | 331 | # OpenCover UI analysis results 332 | OpenCover/ 333 | 334 | # Azure Stream Analytics local run output 335 | ASALocalRun/ 336 | 337 | # MSBuild Binary and Structured Log 338 | *.binlog 339 | 340 | # NVidia Nsight GPU debugger configuration file 341 | *.nvuser 342 | 343 | # MFractors (Xamarin productivity tool) working folder 344 | .mfractor/ 345 | 346 | # Local History for Visual Studio 347 | .localhistory/ 348 | 349 | # BeatPulse healthcheck temp database 350 | healthchecksdb 351 | 352 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 353 | MigrationBackup/ 354 | 355 | # Ionide (cross platform F# VS Code tools) working folder 356 | .ionide/ 357 | 358 | # Fody - auto-generated XML schema 359 | FodyWeavers.xsd -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "src/GitHubActionsVS.sln" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tim Heuer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/timheuer/GitHubActionsVS/actions/workflows/_build.yaml/badge.svg)](https://github.com/timheuer/GitHubActionsVS/actions/workflows/_build.yaml) 2 | [![GitHub last commit](https://img.shields.io/github/last-commit/timheuer/GitHubActionsVS)](https://github.com/timheuer/GitHubActionsVS/) 3 | [![VS Marketplace Badge](https://img.shields.io/visual-studio-marketplace/v/timheuer.GitHubActionsVS?label=VS%20Marketplace&color=purple&logo=visualstudio)](https://marketplace.visualstudio.com/items?itemName=TimHeuer.GitHubActionsVS) 4 | 5 | 6 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) 7 | 8 | 9 | # GitHub Actions for Visual Studio 10 | 11 | The GitHub Actions extension lets you manage your workflows, view the workflow run history, and edit GitHub secrets. 12 | 13 | image 14 | 15 | 16 | ## Features 17 | 18 | This extension mainly serves to provide a quick way to see the GitHub Actions for your open solution if identified as a GitHub.com repo. To view these, either right click on the solution or project in Solution Explorer and click "GitHub Actions" from the menu: 19 | 20 | ![image](https://github.com/timheuer/GitHubActionsVS/assets/4821/ab61f0b9-d82d-41f4-bc29-cfc7507fea7f) 21 | 22 | If an active solution exists and it is both a git and GitHub.com repository, the window will start querying the repository for Actions information on runs and secrets. A progress bar will be shown then you can expand to see the results. 23 | 24 | ### View workflow run history 25 | 26 | To view the history simply select a run and navigate through the tree view to see details. You can double-click on a leaf node to launch to the log point on the repo to view the rich log output. 27 | 28 | If you close and open a new project the window will be refreshed to represent the current state. 29 | 30 | Based on your settings you can enable 'polling' of active running workflows that are not in the `completed` status. This will refresh the Current Branch workflow runs until the state is completed. 31 | 32 | #### Limit run count retrieval 33 | By default a maximum of last 10 runs are retrieved. You can change this in the `Tools...Options` of Visual Studio and set an integer value. 34 | 35 | ![image](https://github.com/timheuer/GitHubActionsVS/assets/4821/661f28cb-b906-476f-ae57-accfbdd63f1f) 36 | 37 | #### Trigger a Workflow 38 | If your Workflows enable a dispatch capability you can trigger to run a workflow directly from Visual Studio: 39 | 40 | ![image](https://github.com/timheuer/GitHubActionsVS/assets/4821/f90697f8-a9e7-4d76-bcad-35d5c4a7ff58) 41 | 42 | #### Manually refresh 43 | You can manually refresh the view by clicking the refresh icon in the toolbar: 44 | 45 | ![image](https://github.com/timheuer/GitHubActionsVS/assets/4821/865d424d-29e1-40e8-96c4-1eeffb458682) 46 | 47 | ### Edit GitHub secrets 48 | The limitation currently is this lists and enables editing of Repository-level secrets (not org or deployment environments yet). 49 | 50 | To add a secret right-click on the Repository Secrets node and select `Add Secret` 51 | 52 | ![image](https://github.com/timheuer/GitHubActionsVS/assets/4821/264acf44-509b-4442-a9b9-80f93ff5cad5) 53 | 54 | This will launch a modal dialog to add the repository secret. This is the same for edit (right-click on an existing secret) which will enable you to edit an existing one or delete. 55 | 56 | ![image](https://github.com/timheuer/GitHubActionsVS/assets/4821/ba12fe8b-9f33-46b7-a4a4-1ae343a0ce34) 57 | 58 | 59 | ## Contributors 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
David Pine
David Pine

💻 📖
Tim Heuer
Tim Heuer

💻 📖
Ivan Zlatanov
Ivan Zlatanov

💻
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | ## Requirements 87 | 88 | Visual Studio 2022 17.6 or later is required to use this extension. Additionally since GitHub Actions is obviously a feature of GitHub, you will need to be attached to an active GitHub.com repository. 89 | 90 | ## Code of Conduct 91 | 92 | This project has adopted the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). For more information see the Code of Conduct itself or contact project maintainers with any additional questions or comments or to report a violation. 93 | -------------------------------------------------------------------------------- /src/.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 | -------------------------------------------------------------------------------- /src/Commands/ActionsToolWindowCommand.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubActionsVS; 2 | 3 | [Command(PackageIds.ActionsCommand)] 4 | internal sealed class ActionsToolWindowCommand : BaseCommand 5 | { 6 | protected override Task ExecuteAsync(OleMenuCmdEventArgs e) 7 | { 8 | return ActionsToolWindow.ShowAsync(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Commands/GotoRepoCommand.cs: -------------------------------------------------------------------------------- 1 | using GitHubActionsVS.ToolWindows; 2 | 3 | namespace GitHubActionsVS; 4 | 5 | [Command(PackageIds.GotoRepoCommand)] 6 | internal sealed class GotoRepoCommand : BaseCommand 7 | { 8 | protected override async Task ExecuteAsync(OleMenuCmdEventArgs e) 9 | { 10 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 11 | ThreadHelper.JoinableTaskFactory.RunAsync(async () => 12 | { 13 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 14 | ToolWindowMessenger messenger = await Package.GetServiceAsync(); 15 | messenger.Send(new(MessageCommand.GotoRepo)); 16 | }).FireAndForget(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Commands/OpenSettingsCommand.cs: -------------------------------------------------------------------------------- 1 | using GitHubActionsVS.ToolWindows; 2 | 3 | namespace GitHubActionsVS; 4 | 5 | [Command(PackageIds.OpenSettingsCommand)] 6 | internal sealed class OpenSettingsCommand : BaseCommand 7 | { 8 | protected override async Task ExecuteAsync(OleMenuCmdEventArgs e) 9 | { 10 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 11 | ThreadHelper.JoinableTaskFactory.RunAsync(async () => 12 | { 13 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 14 | ToolWindowMessenger messenger = await Package.GetServiceAsync(); 15 | messenger.Send(new(MessageCommand.OpenSettings)); 16 | }).FireAndForget(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Commands/RefreshRepoCommand.cs: -------------------------------------------------------------------------------- 1 | using GitHubActionsVS.ToolWindows; 2 | 3 | namespace GitHubActionsVS; 4 | 5 | [Command(PackageIds.RefreshRepoCommand)] 6 | internal sealed class RefreshRepoCommand : BaseCommand 7 | { 8 | protected override async Task ExecuteAsync(OleMenuCmdEventArgs e) 9 | { 10 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 11 | ThreadHelper.JoinableTaskFactory.RunAsync(async () => 12 | { 13 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 14 | ToolWindowMessenger messenger = await Package.GetServiceAsync(); 15 | messenger.Send(new(MessageCommand.Refresh)); 16 | }).FireAndForget(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Commands/ReportFeedbackCommand.cs: -------------------------------------------------------------------------------- 1 | using GitHubActionsVS.ToolWindows; 2 | using Microsoft.VisualStudio.Shell; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace GitHubActionsVS; 10 | 11 | [Command(PackageIds.ReportFeedbackCommand)] 12 | internal sealed class ReportFeedbackCommand : BaseCommand 13 | { 14 | protected override async Task ExecuteAsync(OleMenuCmdEventArgs e) 15 | { 16 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 17 | ThreadHelper.JoinableTaskFactory.RunAsync(async () => 18 | { 19 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 20 | ToolWindowMessenger messenger = await Package.GetServiceAsync(); 21 | 22 | var vsVersion = await VS.Shell.GetVsVersionAsync(); 23 | 24 | messenger.Send(new(MessageCommand.ReportFeedback, vsVersion.ToString())); 25 | }).FireAndForget(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Converters/ConclusionColorConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Data; 3 | using System.Windows.Media; 4 | 5 | namespace GitHubActionsVS.Converters; 6 | 7 | public class ConclusionColorConverter : IMultiValueConverter 8 | { 9 | 10 | public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) 11 | { 12 | string status = values[0] as string; 13 | Brush defaultBrush = values[1] as Brush; 14 | 15 | return GetConclusionColor(status, defaultBrush); 16 | } 17 | 18 | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 19 | { 20 | throw new NotImplementedException(); 21 | } 22 | 23 | private Brush GetConclusionColor(string status, Brush inheritedBrush) => status.ToLowerInvariant() switch 24 | { 25 | 26 | "success" => new SolidColorBrush(Colors.Green), 27 | "failure" => new SolidColorBrush(Colors.Red), 28 | "startup_failure" => new SolidColorBrush(Colors.Red), 29 | "waiting" => new SolidColorBrush(Color.FromRgb(154, 103, 0)), 30 | _ => inheritedBrush, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/Converters/ConclusionIconConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Data; 3 | 4 | namespace GitHubActionsVS.Converters; 5 | public class ConclusionIconConverter : IValueConverter 6 | { 7 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 8 | { 9 | string status = value as string; 10 | return GetConclusionIndicator(status); 11 | } 12 | 13 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 14 | { 15 | return value; 16 | } 17 | 18 | private string GetConclusionIndicator(string status) => status.ToLowerInvariant() switch 19 | { 20 | "success" => "\uEBB3 ", 21 | "completed" => "\uEBB3 ", 22 | "failure" => "\uEC13 ", 23 | "startup_failure" => "\uEC13 ", 24 | "cancelled" => "\uEC19 ", 25 | "skipped" => "\uEABD ", 26 | "pending" => "\uEC15 ", 27 | "queued" => "\uEBA7 ", 28 | "requested" => "\uEBA7 ", 29 | "waiting" => "\uEA82 ", 30 | "inprogress" => "\uEA82 ", 31 | "in_progress" => "\uEA82 ", 32 | "warning" => "\uEC1F ", 33 | null => "\uEA82 ", 34 | _ => "\uEA74 ", 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/Converters/NullToVisibilityConverter.cs: -------------------------------------------------------------------------------- 1 | using GitHubActionsVS.Models; 2 | using System.Globalization; 3 | using System.Windows; 4 | using System.Windows.Data; 5 | 6 | namespace GitHubActionsVS.Converters; 7 | public class NullToVisibilityConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | return value == null ? Visibility.Hidden: Visibility.Visible; 12 | } 13 | 14 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 15 | { 16 | throw new NotImplementedException(); 17 | } 18 | } 19 | 20 | public class NullToBooleanConverter : IValueConverter 21 | { 22 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 23 | { 24 | return value == null ? false : true; 25 | } 26 | 27 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 28 | { 29 | throw new NotImplementedException(); 30 | } 31 | } 32 | 33 | public class BoolToVisibilityConverter : IValueConverter 34 | { 35 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 36 | { 37 | return (bool)value ? Visibility.Visible : Visibility.Collapsed; 38 | } 39 | 40 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 41 | { 42 | throw new NotImplementedException(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/GitHubActionsVS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 5 | latest 6 | 7 | 8 | 9 | Debug 10 | AnyCPU 11 | 2.0 12 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 13 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310} 14 | Library 15 | Properties 16 | GitHubActionsVS 17 | GitHubActionsVS 18 | v4.8 19 | true 20 | true 21 | true 22 | true 23 | false 24 | true 25 | true 26 | Program 27 | $(DevEnvDir)devenv.exe 28 | /rootsuffix Exp 29 | 30 | 31 | true 32 | full 33 | false 34 | bin\Debug\ 35 | DEBUG;TRACE 36 | prompt 37 | 4 38 | 39 | 40 | pdbonly 41 | true 42 | bin\Release\ 43 | TRACE 44 | prompt 45 | 4 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | True 68 | True 69 | UIStrings.resx 70 | 71 | 72 | True 73 | True 74 | source.extension.vsixmanifest 75 | 76 | 77 | GHActionsToolWindow.xaml 78 | 79 | 80 | 81 | 82 | 83 | AddEditSecret.xaml 84 | 85 | 86 | True 87 | True 88 | VSCommandTable.vsct 89 | 90 | 91 | 92 | 93 | Always 94 | true 95 | 96 | 97 | 98 | 99 | 100 | Always 101 | true 102 | 103 | 104 | Designer 105 | VsixManifestGenerator 106 | source.extension.cs 107 | 108 | 109 | true 110 | Always 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | PreserveNewest 119 | true 120 | 121 | 122 | 123 | 124 | Menus.ctmenu 125 | VsctGenerator 126 | VSCommandTable.cs 127 | 128 | 129 | 130 | 131 | 132 | Designer 133 | MSBuild:Compile 134 | 135 | 136 | Designer 137 | MSBuild:Compile 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | compile; build; native; contentfiles; analyzers; buildtransitive 153 | 154 | 155 | 1.0.2 156 | 157 | 158 | 1.1.1 159 | runtime; build; native; contentfiles; analyzers; buildtransitive 160 | all 161 | 162 | 163 | 0.3.4 164 | 165 | 166 | 2.14.1 167 | 168 | 169 | 0.27.2 170 | 171 | 172 | 2.0.320 173 | 174 | 175 | runtime; build; native; contentfiles; analyzers; buildtransitive 176 | all 177 | 178 | 179 | 3.6.133 180 | runtime; build; native; contentfiles; analyzers; buildtransitive 181 | all 182 | 183 | 184 | 7.1.0 185 | 186 | 187 | 1.3.3 188 | 189 | 190 | 191 | 192 | PublicResXFileCodeGenerator 193 | UIStrings.Designer.cs 194 | Designer 195 | 196 | 197 | 198 | 199 | 206 | -------------------------------------------------------------------------------- /src/GitHubActionsVS.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.33723.381 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHubActionsVS", "GitHubActionsVS.csproj", "{FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{87129287-A684-4E35-B65F-C425C13F07F4}" 9 | ProjectSection(SolutionItems) = preProject 10 | ..\.gitignore = ..\.gitignore 11 | ..\README.md = ..\README.md 12 | ..\version.json = ..\version.json 13 | ..\vs-publish.json = ..\vs-publish.json 14 | EndProjectSection 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{8C868368-D330-4780-A683-AF741C00B754}" 17 | ProjectSection(SolutionItems) = preProject 18 | ..\.github\workflows\pr.yaml = ..\.github\workflows\pr.yaml 19 | ..\.github\workflows\publish.yaml = ..\.github\workflows\publish.yaml 20 | ..\.github\workflows\_build.yaml = ..\.github\workflows\_build.yaml 21 | EndProjectSection 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Debug|arm64 = Debug|arm64 27 | Debug|x86 = Debug|x86 28 | Release|Any CPU = Release|Any CPU 29 | Release|arm64 = Release|arm64 30 | Release|x86 = Release|x86 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Debug|arm64.ActiveCfg = Debug|arm64 36 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Debug|arm64.Build.0 = Debug|arm64 37 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Debug|x86.ActiveCfg = Debug|x86 38 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Debug|x86.Build.0 = Debug|x86 39 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Release|arm64.ActiveCfg = Release|arm64 42 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Release|arm64.Build.0 = Release|arm64 43 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Release|x86.ActiveCfg = Release|x86 44 | {FBFFEFBA-F117-44A7-ACA1-3ECE4CC42310}.Release|x86.Build.0 = Release|x86 45 | EndGlobalSection 46 | GlobalSection(SolutionProperties) = preSolution 47 | HideSolutionNode = FALSE 48 | EndGlobalSection 49 | GlobalSection(ExtensibilityGlobals) = postSolution 50 | SolutionGuid = {5543C1A6-7F82-45F0-9449-3CF513D538B0} 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /src/GitHubActionsVSPackage.cs: -------------------------------------------------------------------------------- 1 | global using Community.VisualStudio.Toolkit; 2 | global using Microsoft.VisualStudio.Shell; 3 | global using System; 4 | global using Task = System.Threading.Tasks.Task; 5 | using GitHubActionsVS.ToolWindows; 6 | using Microsoft.VisualStudio; 7 | using Microsoft.VisualStudio.Shell.Interop; 8 | using System.Runtime.InteropServices; 9 | using System.Threading; 10 | 11 | namespace GitHubActionsVS; 12 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 13 | [ProvideAutoLoad(VSConstants.UICONTEXT.SolutionHasSingleProject_string, PackageAutoLoadFlags.BackgroundLoad)] 14 | [ProvideAutoLoad(VSConstants.UICONTEXT.SolutionHasMultipleProjects_string, PackageAutoLoadFlags.BackgroundLoad)] 15 | [InstalledProductRegistration(Vsix.Name, Vsix.Description, Vsix.Version)] 16 | [ProvideToolWindow(typeof(ActionsToolWindow.Pane), Style = VsDockStyle.Tabbed, Window = WindowGuids.SolutionExplorer)] 17 | [ProvideOptionPage(typeof(OptionsProvider.ExtensionOptionsOptions), "GitHub", "Actions", 0, 0, true, SupportsProfiles = true)] 18 | [ProvideMenuResource("Menus.ctmenu", 1)] 19 | [Guid(PackageGuids.GitHubActionsVSString)] 20 | [ProvideBindingPath] 21 | [ProvideService(typeof(ToolWindowMessenger), IsAsyncQueryable = true)] 22 | public sealed class GitHubActionsVSPackage : ToolkitPackage, IVsSolutionEvents 23 | { 24 | private IVsSolution _solution; 25 | private uint _cookie; 26 | 27 | protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) 28 | { 29 | AddService(typeof(ToolWindowMessenger), (_, _, _) => Task.FromResult(new ToolWindowMessenger())); 30 | 31 | await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); 32 | 33 | _solution = await GetServiceAsync(typeof(SVsSolution)) as IVsSolution; 34 | _solution?.AdviseSolutionEvents(this, out _cookie); 35 | 36 | await this.RegisterCommandsAsync(); 37 | 38 | this.RegisterToolWindows(); 39 | } 40 | 41 | protected override void Dispose(bool disposing) 42 | { 43 | ThreadHelper.ThrowIfNotOnUIThread(); 44 | if (_solution is not null) 45 | { 46 | _solution.UnadviseSolutionEvents(_cookie); 47 | _solution = null; 48 | } 49 | base.Dispose(disposing); 50 | } 51 | 52 | public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution) 53 | { 54 | if (ActionsToolWindow.Instance is { ActionsWindow: { } } window) 55 | { 56 | _ = window.ActionsWindow.GetRepoInfoAsync(); 57 | } 58 | 59 | return VSConstants.S_OK; 60 | } 61 | 62 | public int OnBeforeCloseSolution(object pUnkReserved) 63 | { 64 | if (ActionsToolWindow.Instance is { ActionsWindow: { } } window) 65 | { 66 | window.ActionsWindow.ResetTrees(); 67 | } 68 | 69 | return VSConstants.S_OK; 70 | } 71 | 72 | // Implement other IVsSolutionEvents methods with empty bodies 73 | public int OnAfterCloseSolution(object pUnkReserved) => VSConstants.S_OK; 74 | public int OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy) => VSConstants.S_OK; 75 | public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) => VSConstants.S_OK; 76 | public int OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved) => VSConstants.S_OK; 77 | public int OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy) => VSConstants.S_OK; 78 | public int OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel) => VSConstants.S_OK; 79 | public int OnQueryCloseSolution(object pUnkReserved, ref int pfCancel) => VSConstants.S_OK; 80 | public int OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel) => VSConstants.S_OK; 81 | } -------------------------------------------------------------------------------- /src/Helpers/ConclusionFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace GitHubActionsVS.Helpers; 8 | public class ConclusionFilter 9 | { 10 | public static bool IsFinished(string conclusion) 11 | { 12 | return conclusion.ToLower() switch 13 | { 14 | "pending" or "waiting" or "queued" or "in_progress" or "inprogress" or "requested" or null => false, 15 | _ => true, 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Helpers/CredentialManager.cs: -------------------------------------------------------------------------------- 1 | using CredentialManagement; 2 | 3 | namespace GitHubActionsVS.Helpers; 4 | internal class CredentialManager 5 | { 6 | public static UserPass GetCredentials(string target) 7 | { 8 | using var cm = new Credential { Target = target }; 9 | if (!cm.Exists()) 10 | { 11 | return null; 12 | } 13 | 14 | cm.Load(); 15 | return new UserPass { Username = cm.Username, Password = cm.Password }; 16 | } 17 | 18 | public class UserPass 19 | { 20 | public string Username { get; set; } 21 | public string Password { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Helpers/RepoInfo.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | 4 | namespace GitHubActionsVS.Helpers; 5 | internal class RepoInfo 6 | { 7 | public bool IsGitHub { get; set; } 8 | public string RepoName { get; set; } 9 | public string RepoOwner { get; set; } 10 | public string RepoUrl { get; set; } 11 | public string CurrentBranch { get; set; } 12 | 13 | 14 | internal void FindGitFolder(string path, out string foundPath) 15 | { 16 | foundPath = null; 17 | // Check if the current directory contains a .git folder 18 | if (Directory.Exists(Path.Combine(path, ".git"))) 19 | { 20 | foundPath = path; 21 | var repo = new LibGit2Sharp.Repository(foundPath); 22 | var remote = repo.Network.Remotes.FirstOrDefault(); 23 | if (remote is not null) 24 | { 25 | var url = remote.Url; 26 | if (url.Contains("github.com")) 27 | { 28 | IsGitHub = true; 29 | var parts = url.Split('/'); 30 | RepoOwner = parts[parts.Length - 2]; 31 | RepoName = parts[parts.Length - 1].Replace(".git", ""); 32 | RepoUrl = url.Replace(".git", ""); 33 | CurrentBranch = repo.Head.FriendlyName; 34 | } 35 | } 36 | return; 37 | } 38 | else 39 | { 40 | string parentPath = Directory.GetParent(path)?.FullName; 41 | if (!string.IsNullOrEmpty(parentPath)) 42 | { 43 | FindGitFolder(parentPath, out foundPath); // Recursively search the parent directory 44 | } 45 | } 46 | return; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tim Heuer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/Models/BaseWorkflowType.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubActionsVS.Models; 2 | 3 | public abstract class BaseWorkflowType 4 | { 5 | public string Name { get; set; } 6 | 7 | public abstract string DisplayName { get; } 8 | public string Conclusion { get; set; } 9 | public DateTimeOffset? LogDate { get; set; } 10 | public string DisplayDate => $"{LogDate:g}"; 11 | public string? Url { get; set; } 12 | public string Id { get; set; } 13 | public string TriggerEvent { get; set; } 14 | public string TriggerLogin { get; set; } 15 | public string RunDuration { get; set; } 16 | 17 | public bool HasActions 18 | { 19 | get 20 | { 21 | return TriggerEvent is not null || Url is not null; 22 | } 23 | } 24 | 25 | public bool Cancellable 26 | { 27 | get 28 | { 29 | return TriggerEvent is not null && !Helpers.ConclusionFilter.IsFinished(Conclusion); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Models/SimpleEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace GitHubActionsVS.Models; 4 | public class SimpleEnvironment 5 | { 6 | public string Name { get; set; } 7 | public string Url { get; set; } 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Models/SimpleJob.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubActionsVS.Models; 2 | 3 | public class SimpleJob : SimpleRun 4 | { 5 | public override string DisplayName => Name; 6 | } 7 | -------------------------------------------------------------------------------- /src/Models/SimpleRun.cs: -------------------------------------------------------------------------------- 1 | using Humanizer; 2 | using System.Collections.Generic; 3 | 4 | namespace GitHubActionsVS.Models; 5 | public class SimpleRun : BaseWorkflowType 6 | { 7 | public List Jobs { get; set; } 8 | public string RunNumber { get; set; } 9 | 10 | public override string DisplayName => $"{Name} #{RunNumber} ({LogDate.Humanize()})"; 11 | } 12 | -------------------------------------------------------------------------------- /src/Options/ExtensionOptions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace GitHubActionsVS; 5 | internal partial class OptionsProvider 6 | { 7 | // Register the options with this attribute on your package class: 8 | // [ProvideOptionPage(typeof(OptionsProvider.ExtensionOptionsOptions), "GitHubActionsVS", "ExtensionOptions", 0, 0, true, SupportsProfiles = true)] 9 | [ComVisible(true)] 10 | public class ExtensionOptionsOptions : BaseOptionPage { } 11 | } 12 | 13 | public class ExtensionOptions : BaseOptionModel, IRatingConfig 14 | { 15 | [Category("Query Settings")] 16 | [DisplayName("Max Runs")] 17 | [Description("The maximum number of runs to retrieve")] 18 | [DefaultValue(10)] 19 | public int MaxRuns { get; set; } = 10; 20 | 21 | [Category("Query Settings")] 22 | [DisplayName("Refresh Active Jobs")] 23 | [Description("Whether to poll/refresh when pending/active jobs are going")] 24 | [DefaultValue(false)] 25 | public bool RefreshActiveJobs { get; set; } = false; 26 | 27 | [Category("Query Settings")] 28 | [DisplayName("Refresh Interval (in seconds)")] 29 | [Description("The interval (in seconds) to poll/refresh when pending/active jobs are going")] 30 | [DefaultValue(5)] 31 | public int RefreshInterval { get; set; } = 5; 32 | 33 | [Browsable(false)] 34 | public int RatingRequests { get; set; } 35 | } 36 | -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using GitHubActionsVS; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle(Vsix.Name)] 6 | [assembly: AssemblyDescription(Vsix.Description)] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany(Vsix.Author)] 9 | [assembly: AssemblyProduct(Vsix.Name)] 10 | [assembly: AssemblyCopyright(Vsix.Author)] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\LibGit2Sharp.dll")] 17 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\Sodium.Core.dll")] 18 | 19 | namespace System.Runtime.CompilerServices; 20 | public class IsExternalInit { } -------------------------------------------------------------------------------- /src/Resources/AddItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timheuer/GitHubActionsVS/f7ee61b70bf27f5328896bff8200cc5a5860136f/src/Resources/AddItem.png -------------------------------------------------------------------------------- /src/Resources/CancelBuild.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timheuer/GitHubActionsVS/f7ee61b70bf27f5328896bff8200cc5a5860136f/src/Resources/CancelBuild.png -------------------------------------------------------------------------------- /src/Resources/Delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timheuer/GitHubActionsVS/f7ee61b70bf27f5328896bff8200cc5a5860136f/src/Resources/Delete.png -------------------------------------------------------------------------------- /src/Resources/Edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timheuer/GitHubActionsVS/f7ee61b70bf27f5328896bff8200cc5a5860136f/src/Resources/Edit.png -------------------------------------------------------------------------------- /src/Resources/GitHub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timheuer/GitHubActionsVS/f7ee61b70bf27f5328896bff8200cc5a5860136f/src/Resources/GitHub.png -------------------------------------------------------------------------------- /src/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timheuer/GitHubActionsVS/f7ee61b70bf27f5328896bff8200cc5a5860136f/src/Resources/Icon.png -------------------------------------------------------------------------------- /src/Resources/OpenWebSite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timheuer/GitHubActionsVS/f7ee61b70bf27f5328896bff8200cc5a5860136f/src/Resources/OpenWebSite.png -------------------------------------------------------------------------------- /src/Resources/Run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timheuer/GitHubActionsVS/f7ee61b70bf27f5328896bff8200cc5a5860136f/src/Resources/Run.png -------------------------------------------------------------------------------- /src/Resources/UIStrings.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 GitHubActionsVS.Resources { 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", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | public class UIStrings { 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 UIStrings() { 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 | public 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("GitHubActionsVS.Resources.UIStrings", typeof(UIStrings).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 | public 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 string similar to Add Secret. 65 | /// 66 | public static string ADD_SECRET { 67 | get { 68 | return ResourceManager.GetString("ADD_SECRET", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Cancel. 74 | /// 75 | public static string BUTTON_CANCEL { 76 | get { 77 | return ResourceManager.GetString("BUTTON_CANCEL", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Create. 83 | /// 84 | public static string BUTTON_SAVE { 85 | get { 86 | return ResourceManager.GetString("BUTTON_SAVE", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to Update. 92 | /// 93 | public static string BUTTON_UPDATE { 94 | get { 95 | return ResourceManager.GetString("BUTTON_UPDATE", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to Cancel Run. 101 | /// 102 | public static string CANCEL_RUN { 103 | get { 104 | return ResourceManager.GetString("CANCEL_RUN", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to Are you sure you want to delete this secret?. 110 | /// 111 | public static string CONFIRM_DELETE { 112 | get { 113 | return ResourceManager.GetString("CONFIRM_DELETE", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to Confirm Delete. 119 | /// 120 | public static string CONFIRM_DELETE_TITLE { 121 | get { 122 | return ResourceManager.GetString("CONFIRM_DELETE_TITLE", resourceCulture); 123 | } 124 | } 125 | 126 | /// 127 | /// Looks up a localized string similar to Delete Secret. 128 | /// 129 | public static string DELETE_SECRET { 130 | get { 131 | return ResourceManager.GetString("DELETE_SECRET", resourceCulture); 132 | } 133 | } 134 | 135 | /// 136 | /// Looks up a localized string similar to Edit Secret. 137 | /// 138 | public static string EDIT_SECRET { 139 | get { 140 | return ResourceManager.GetString("EDIT_SECRET", resourceCulture); 141 | } 142 | } 143 | 144 | /// 145 | /// Looks up a localized string similar to Repository found, but not identified as a GitHub repo. Please open a folder that contains a GitHub repository.. 146 | /// 147 | public static string GIT_NOT_GITHUB { 148 | get { 149 | return ResourceManager.GetString("GIT_NOT_GITHUB", resourceCulture); 150 | } 151 | } 152 | 153 | /// 154 | /// Looks up a localized string similar to Current Branch. 155 | /// 156 | public static string HEADER_CURRENT_BRANCH { 157 | get { 158 | return ResourceManager.GetString("HEADER_CURRENT_BRANCH", resourceCulture); 159 | } 160 | } 161 | 162 | /// 163 | /// Looks up a localized string similar to Environments. 164 | /// 165 | public static string HEADER_ENVIRONMENTS { 166 | get { 167 | return ResourceManager.GetString("HEADER_ENVIRONMENTS", resourceCulture); 168 | } 169 | } 170 | 171 | /// 172 | /// Looks up a localized string similar to Repository Secrets. 173 | /// 174 | public static string HEADER_REPO_SECRETS { 175 | get { 176 | return ResourceManager.GetString("HEADER_REPO_SECRETS", resourceCulture); 177 | } 178 | } 179 | 180 | /// 181 | /// Looks up a localized string similar to Secrets. 182 | /// 183 | public static string HEADER_SECRETS { 184 | get { 185 | return ResourceManager.GetString("HEADER_SECRETS", resourceCulture); 186 | } 187 | } 188 | 189 | /// 190 | /// Looks up a localized string similar to Settings. 191 | /// 192 | public static string HEADER_SETTINGS { 193 | get { 194 | return ResourceManager.GetString("HEADER_SETTINGS", resourceCulture); 195 | } 196 | } 197 | 198 | /// 199 | /// Looks up a localized string similar to Workflows. 200 | /// 201 | public static string HEADER_WORKFLOWS { 202 | get { 203 | return ResourceManager.GetString("HEADER_WORKFLOWS", resourceCulture); 204 | } 205 | } 206 | 207 | /// 208 | /// Looks up a localized string similar to Insufficient permissions to retrieve Secrets. 209 | /// 210 | public static string INSUFFICIENT_SECRET_PERMS { 211 | get { 212 | return ResourceManager.GetString("INSUFFICIENT_SECRET_PERMS", resourceCulture); 213 | } 214 | } 215 | 216 | /// 217 | /// Looks up a localized string similar to Name:. 218 | /// 219 | public static string LABEL_NAME { 220 | get { 221 | return ResourceManager.GetString("LABEL_NAME", resourceCulture); 222 | } 223 | } 224 | 225 | /// 226 | /// Looks up a localized string similar to Secret:. 227 | /// 228 | public static string LABEL_SECRET { 229 | get { 230 | return ResourceManager.GetString("LABEL_SECRET", resourceCulture); 231 | } 232 | } 233 | 234 | /// 235 | /// Looks up a localized string similar to No environments defined. 236 | /// 237 | public static string NO_ENV { 238 | get { 239 | return ResourceManager.GetString("NO_ENV", resourceCulture); 240 | } 241 | } 242 | 243 | /// 244 | /// Looks up a localized string similar to No GitHub repositories found. Please open a folder that contains a GitHub repository.. 245 | /// 246 | public static string NO_GIT_REPO { 247 | get { 248 | return ResourceManager.GetString("NO_GIT_REPO", resourceCulture); 249 | } 250 | } 251 | 252 | /// 253 | /// Looks up a localized string similar to No project or solution loaded. 254 | /// 255 | public static string NO_PROJ_LOADED { 256 | get { 257 | return ResourceManager.GetString("NO_PROJ_LOADED", resourceCulture); 258 | } 259 | } 260 | 261 | /// 262 | /// Looks up a localized string similar to No repository secrets defined. 263 | /// 264 | public static string NO_REPO_SECRETS { 265 | get { 266 | return ResourceManager.GetString("NO_REPO_SECRETS", resourceCulture); 267 | } 268 | } 269 | 270 | /// 271 | /// Looks up a localized string similar to No workflow runs found for query. 272 | /// 273 | public static string NO_WORKFLOW_RUNS { 274 | get { 275 | return ResourceManager.GetString("NO_WORKFLOW_RUNS", resourceCulture); 276 | } 277 | } 278 | 279 | /// 280 | /// Looks up a localized string similar to Run Workflow. 281 | /// 282 | public static string RUN_WORKFLOW { 283 | get { 284 | return ResourceManager.GetString("RUN_WORKFLOW", resourceCulture); 285 | } 286 | } 287 | 288 | /// 289 | /// Looks up a localized string similar to by. 290 | /// 291 | public static string TRIGGERED_BY { 292 | get { 293 | return ResourceManager.GetString("TRIGGERED_BY", resourceCulture); 294 | } 295 | } 296 | 297 | /// 298 | /// Looks up a localized string similar to Triggered via. 299 | /// 300 | public static string TRIGGERED_VIA { 301 | get { 302 | return ResourceManager.GetString("TRIGGERED_VIA", resourceCulture); 303 | } 304 | } 305 | 306 | /// 307 | /// Looks up a localized string similar to View Log. 308 | /// 309 | public static string VIEW_LOG { 310 | get { 311 | return ResourceManager.GetString("VIEW_LOG", resourceCulture); 312 | } 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/Resources/UIStrings.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 | Add Secret 122 | Menu for adding a secret 123 | 124 | 125 | Cancel 126 | Button text for cancel 127 | 128 | 129 | Create 130 | Button text for save 131 | 132 | 133 | Update 134 | Button text for update 135 | 136 | 137 | Cancel Run 138 | Menu for cancelling a run 139 | 140 | 141 | Are you sure you want to delete this secret? 142 | Question for deleting an item 143 | 144 | 145 | Confirm Delete 146 | Title bar for confirm delete 147 | 148 | 149 | Delete Secret 150 | Menu for deleting a secret 151 | 152 | 153 | Edit Secret 154 | Menu for editing a secret 155 | 156 | 157 | Repository found, but not identified as a GitHub repo. Please open a folder that contains a GitHub repository. 158 | For when the git repo is not github.com 159 | 160 | 161 | Current Branch 162 | Current Branch text formatted with current branch 163 | 164 | 165 | Environments 166 | Header for environments 167 | 168 | 169 | Repository Secrets 170 | Header for repo secrets 171 | 172 | 173 | Secrets 174 | Header for secrets 175 | 176 | 177 | Settings 178 | Settings expander header 179 | 180 | 181 | Workflows 182 | Header for workflow top-level node 183 | 184 | 185 | Insufficient permissions to retrieve Secrets 186 | Error for when you don't have perms 187 | 188 | 189 | Name: 190 | Label for secret form for name 191 | 192 | 193 | Secret: 194 | Label for secret form for secret 195 | 196 | 197 | No environments defined 198 | For when no environments defined 199 | 200 | 201 | No GitHub repositories found. Please open a folder that contains a GitHub repository. 202 | For when no git repo is found 203 | 204 | 205 | No project or solution loaded 206 | For when no project is loaded 207 | 208 | 209 | No repository secrets defined 210 | For when no repo secrets defined 211 | 212 | 213 | No workflow runs found for query 214 | For when no workload runs are found 215 | 216 | 217 | Run Workflow 218 | Menu for running a workflow 219 | 220 | 221 | by 222 | Message separating event and actor - requries spaces 223 | 224 | 225 | Triggered via 226 | Message starting the trigger sentence 227 | 228 | 229 | View Log 230 | Menu for viewing a log 231 | 232 | -------------------------------------------------------------------------------- /src/Resources/codicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timheuer/GitHubActionsVS/f7ee61b70bf27f5328896bff8200cc5a5860136f/src/Resources/codicon.ttf -------------------------------------------------------------------------------- /src/ToolWindows/ActionsToolWindow.cs: -------------------------------------------------------------------------------- 1 | using GitHubActionsVS.ToolWindows; 2 | using Microsoft.VisualStudio.Imaging; 3 | using System.Runtime.InteropServices; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | 8 | namespace GitHubActionsVS; 9 | public class ActionsToolWindow : BaseToolWindow 10 | { 11 | public override string GetTitle(int toolWindowId) => "GitHub Actions"; 12 | 13 | public override Type PaneType => typeof(Pane); 14 | 15 | public GHActionsToolWindow ActionsWindow { get; private set; } 16 | private static ActionsToolWindow _instance; 17 | public static ActionsToolWindow Instance => _instance ??= new ActionsToolWindow(); 18 | 19 | public override async Task CreateAsync(int toolWindowId, CancellationToken cancellationToken) 20 | { 21 | ToolWindowMessenger toolWindowMessenger = await Package.GetServiceAsync(); 22 | 23 | ActionsWindow = new GHActionsToolWindow(toolWindowMessenger); 24 | _instance = this; 25 | return ActionsWindow; 26 | } 27 | 28 | [Guid("4a4ad204-3623-4e03-b2d1-6fef94652174")] 29 | internal class Pane : ToolkitToolWindowPane 30 | { 31 | public Pane() 32 | { 33 | BitmapImageMoniker = KnownMonikers.ToolWindow; 34 | ToolBar = new System.ComponentModel.Design.CommandID(PackageGuids.GitHubActionsVS, PackageIds.TWindowToolbar); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/ToolWindows/GHActionsToolWindow.xaml: -------------------------------------------------------------------------------- 1 |  20 | 21 | pack://application:,,,/GitHubActionsVS;component/Resources/#codicon 22 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 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 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /src/ToolWindows/GHActionsToolWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using GitHubActionsVS.Helpers; 2 | using GitHubActionsVS.Models; 3 | using GitHubActionsVS.ToolWindows; 4 | using Octokit; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.Threading.Tasks; 8 | using System.Windows; 9 | using System.Windows.Controls; 10 | using System.Windows.Input; 11 | using GitHubActionsVS.UserControls; 12 | using Application = System.Windows.Application; 13 | using System.Windows.Media; 14 | using MessageBox = Community.VisualStudio.Toolkit.MessageBox; 15 | using resx = GitHubActionsVS.Resources.UIStrings; 16 | using Humanizer; 17 | 18 | namespace GitHubActionsVS; 19 | 20 | /// 21 | /// Interaction logic for GHActionsToolWindow.xaml 22 | /// 23 | public partial class GHActionsToolWindow : UserControl 24 | { 25 | private readonly RepoInfo _repoInfo = null; 26 | private readonly ToolWindowMessenger _toolWindowMessenger = null; 27 | private int maxRuns = 10; 28 | private bool refreshPending = false; 29 | private int refreshInterval = 5; 30 | OutputWindowPane _pane; 31 | 32 | public GHActionsToolWindow(ToolWindowMessenger toolWindowMessenger) 33 | { 34 | 35 | _toolWindowMessenger = toolWindowMessenger ??= new(); 36 | toolWindowMessenger.MessageReceived += OnMessageReceived; 37 | InitializeComponent(); 38 | _repoInfo = new(); 39 | 40 | _ = GetRepoInfoAsync(); 41 | } 42 | 43 | private void OnMessageReceived(object sender, MessagePayload payload) 44 | { 45 | ThreadHelper.JoinableTaskFactory.RunAsync(async () => 46 | { 47 | var (command, text) = payload; 48 | await (command switch 49 | { 50 | MessageCommand.Refresh => GetRepoInfoAsync(), 51 | MessageCommand.GotoRepo => GotoRepoAsync(), 52 | MessageCommand.OpenSettings => OpenSettingsAsync(), 53 | MessageCommand.ReportFeedback => ReportFeedbackAsync(text), 54 | _ => Task.CompletedTask 55 | }); 56 | }).FireAndForget(); 57 | } 58 | 59 | private Task ReportFeedbackAsync(string text) 60 | { 61 | Process.Start($"https://github.com/timheuer/GitHubActionsVS/issues/new?assignees=timheuer&labels=bug&projects=&template=bug_report.yaml&title=%5BBUG%5D%3A+&vsversion={text}"); 62 | 63 | return Task.CompletedTask; 64 | } 65 | 66 | private async Task OpenSettingsAsync() 67 | { 68 | await VS.Settings.OpenAsync(typeof(OptionsProvider.ExtensionOptionsOptions)); 69 | } 70 | 71 | private Task GotoRepoAsync() 72 | { 73 | if (_repoInfo is { RepoUrl.Length: > 0 }) 74 | { 75 | Process.Start($"{_repoInfo?.RepoUrl}/actions"); 76 | } 77 | 78 | return Task.CompletedTask; 79 | } 80 | 81 | public void ResetTrees() => ClearTreeViews(); 82 | 83 | public async Task GetRepoInfoAsync() 84 | { 85 | if (_pane is null) 86 | { 87 | _pane = await VS.Windows.CreateOutputWindowPaneAsync("GitHub Actions for VS"); 88 | } 89 | 90 | ClearTreeViews(); 91 | _repoInfo.RepoOwner = null; 92 | _repoInfo.RepoName = null; 93 | _repoInfo.IsGitHub = false; 94 | _repoInfo.RepoUrl = null; 95 | 96 | // get settings 97 | var generalSettings = await ExtensionOptions.GetLiveInstanceAsync(); 98 | maxRuns = generalSettings.MaxRuns; 99 | refreshInterval = generalSettings.RefreshInterval; 100 | refreshPending = generalSettings.RefreshActiveJobs; 101 | 102 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Extension settings retrieved and applied"); 103 | 104 | // find the git folder 105 | var solution = await VS.Solutions.GetCurrentSolutionAsync(); 106 | if (solution is null) 107 | { 108 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] No solution found"); 109 | Debug.WriteLine("No solution found"); 110 | ShowInfoMessage(resx.NO_PROJ_LOADED); 111 | return; 112 | } 113 | var projectPath = solution?.FullPath; 114 | 115 | _repoInfo.FindGitFolder(projectPath, out string gitPath); 116 | 117 | if (string.IsNullOrWhiteSpace(gitPath)) 118 | { 119 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] No git repo found"); 120 | Debug.WriteLine("No git repo found"); 121 | ShowInfoMessage(resx.NO_GIT_REPO); 122 | } 123 | else 124 | { 125 | Debug.WriteLine($"Found git repo at {gitPath}"); 126 | if (_repoInfo.IsGitHub) 127 | { 128 | Debug.WriteLine($"GitHub repo: {_repoInfo.RepoOwner}/{_repoInfo.RepoName}"); 129 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Found repo for {gitPath} at {_repoInfo.RepoOwner}/{_repoInfo.RepoName}"); 130 | await LoadDataAsync(); 131 | } 132 | else 133 | { 134 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Not a GitHub repo"); 135 | Debug.WriteLine("Not a GitHub repo"); 136 | ShowInfoMessage(resx.GIT_NOT_GITHUB); 137 | } 138 | } 139 | } 140 | 141 | private void ShowInfoMessage(string messageString) 142 | { 143 | ThreadHelper.JoinableTaskFactory.Run(async () => 144 | { 145 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 146 | MessageArea.Text = messageString; 147 | MessageArea.Visibility = Visibility.Visible; 148 | ActionsInfoPanel.Visibility = Visibility.Collapsed; 149 | }); 150 | } 151 | 152 | private void ClearTreeViews() 153 | { 154 | tvSecrets.ItemsSource = null; 155 | tvSecrets.Header = resx.HEADER_REPO_SECRETS; 156 | tvEnvironments.ItemsSource = null; 157 | tvEnvironments.Header = resx.HEADER_ENVIRONMENTS; 158 | tvCurrentBranch.ItemsSource = null; 159 | tvWorkflows.ItemsSource = null; 160 | CurrentBranchExpander.IsExpanded = false; 161 | } 162 | 163 | private async Task LoadDataAsync() 164 | { 165 | // Setup ratings prompt 166 | ExtensionOptions options = await ExtensionOptions.GetLiveInstanceAsync(); 167 | RatingPrompt prompt = new("TimHeuer.GitHubActionsVS", Vsix.Name, options, 4); 168 | prompt.RegisterSuccessfulUsage(); 169 | 170 | MessageArea.Visibility = Visibility.Collapsed; 171 | ActionsInfoPanel.Visibility = Visibility.Visible; 172 | 173 | refreshProgress.IsIndeterminate = true; 174 | refreshProgress.Visibility = Visibility.Visible; 175 | 176 | GitHubClient client = GetGitHubClient(); 177 | 178 | try 179 | { 180 | // get secrets 181 | await RefreshSecretsAsync(client); 182 | // get environments 183 | await RefreshEnvironmentsAsync(client); 184 | // get workflows 185 | await RefreshWorkflowsAsync(client); 186 | 187 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Loading Workflow Runs..."); 188 | // get current branch 189 | var runs = await client.Actions?.Workflows?.Runs?.List(_repoInfo.RepoOwner, _repoInfo.RepoName, new WorkflowRunsRequest() { Branch = _repoInfo.CurrentBranch }, new ApiOptions() { PageCount = 1, PageSize = maxRuns }); 190 | 191 | List runsList = new List(); 192 | 193 | if (runs.TotalCount > 0) 194 | { 195 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Number of runs found: {runs.TotalCount}"); 196 | // creating simplified model of the GH info for the treeview 197 | 198 | // iterate throught the runs 199 | foreach (var run in runs.WorkflowRuns) 200 | { 201 | SimpleRun simpleRun = new() 202 | { 203 | Conclusion = run.Conclusion is not null ? run.Conclusion.Value.StringValue.Humanize(LetterCasing.Title) : run.Status.StringValue.Humanize(LetterCasing.Title), 204 | Name = run.Name, 205 | LogDate = run.UpdatedAt, 206 | Id = run.Id.ToString(), 207 | RunNumber = run.RunNumber.ToString(), 208 | TriggerEvent = run.Event, 209 | TriggerLogin = run.TriggeringActor.Login, 210 | RunDuration = (run.UpdatedAt - run.RunStartedAt).Humanize(2) 211 | }; 212 | 213 | if (refreshPending) 214 | { 215 | var timer = new System.Timers.Timer(refreshInterval*1000); 216 | timer.Elapsed += async (sender, e) => 217 | { 218 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 219 | await LoadDataAsync(); 220 | }; 221 | timer.AutoReset = false; 222 | 223 | if (((run.Status == WorkflowRunStatus.Queued) || (run.Status == WorkflowRunStatus.InProgress) || (run.Status == WorkflowRunStatus.Pending) || (run.Status == WorkflowRunStatus.Waiting))) 224 | { 225 | timer.Start(); 226 | } 227 | } 228 | 229 | // get the jobs for the run 230 | var jobs = await client.Actions.Workflows.Jobs?.List(_repoInfo.RepoOwner, _repoInfo.RepoName, run.Id); 231 | 232 | List simpleJobs = new(); 233 | 234 | // iterate through the jobs' steps 235 | foreach (var job in jobs.Jobs) 236 | { 237 | List steps = new(); 238 | foreach (var step in job.Steps) 239 | { 240 | steps.Add(new SimpleJob() 241 | { 242 | Conclusion = step.Conclusion is not null ? step.Conclusion.Value.StringValue : step.Status.StringValue, 243 | Name = step.Name, 244 | Url = $"{job.HtmlUrl}#step:{step.Number.ToString()}:1" 245 | }); 246 | } 247 | simpleJobs.Add(new SimpleJob() 248 | { 249 | Conclusion = job.Conclusion is not null ? job.Conclusion.Value.StringValue : job.Status.StringValue, 250 | Name = job.Name, 251 | Id = job.Id.ToString(), 252 | Jobs = steps // add the steps to the job 253 | }); 254 | } 255 | 256 | // add the jobs to the run 257 | simpleRun.Jobs = simpleJobs; 258 | 259 | runsList.Add(simpleRun); 260 | } 261 | } 262 | else 263 | { 264 | // no runs found 265 | var noRunsItem = new SimpleRun 266 | { 267 | Name = resx.NO_WORKFLOW_RUNS, 268 | Conclusion = "warning", 269 | LogDate = DateTime.Now, 270 | RunNumber = "N/A" 271 | }; 272 | runsList.Add(noRunsItem); 273 | } 274 | 275 | tvCurrentBranch.ItemsSource = runsList; 276 | } 277 | catch (ApiException ex) 278 | { 279 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Error retrieving Workflow Runs: {ex.Message}:{ex.StatusCode}"); 280 | await ex.LogAsync(); 281 | } 282 | catch (Exception ex) 283 | { 284 | await ex.LogAsync(); 285 | } 286 | 287 | CurrentBranchExpander.IsExpanded = true; 288 | refreshProgress.Visibility = Visibility.Hidden; 289 | refreshProgress.IsIndeterminate = false; 290 | } 291 | 292 | private async Task RefreshEnvironmentsAsync(GitHubClient client) 293 | { 294 | List envList = new List(); 295 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Loading Environments..."); 296 | try 297 | { 298 | var repoEnvs = await client.Repository?.Environment?.GetAll(_repoInfo.RepoOwner, _repoInfo.RepoName); 299 | 300 | if (repoEnvs.TotalCount > 0) 301 | { 302 | tvEnvironments.Header = $"{resx.HEADER_ENVIRONMENTS} ({repoEnvs.TotalCount})"; 303 | foreach (var env in repoEnvs.Environments) 304 | { 305 | var envItem = new SimpleEnvironment 306 | { 307 | Name = env.Name, 308 | Url = env.HtmlUrl 309 | }; 310 | 311 | envList.Add(envItem); 312 | } 313 | } 314 | else 315 | { 316 | envList.Add(new() { Name = resx.NO_ENV }); 317 | } 318 | } 319 | catch (ApiException ex) 320 | { 321 | if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized || ex.StatusCode == System.Net.HttpStatusCode.Forbidden) 322 | { 323 | envList.Add(new SimpleEnvironment() { Name = resx.INSUFFICIENT_SECRET_PERMS }); 324 | await ex.LogAsync(ex.Message); 325 | } 326 | } 327 | catch (Exception ex) 328 | { 329 | envList.Add(new SimpleEnvironment() { Name = "Unable to retrieve Environments, please check logs" }); 330 | await ex.LogAsync(); 331 | } 332 | 333 | tvEnvironments.ItemsSource = envList; 334 | } 335 | 336 | private async Task RefreshWorkflowsAsync(GitHubClient client) 337 | { 338 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Loading Workflows..."); 339 | try 340 | { 341 | var workflows = await client.Actions?.Workflows?.List(_repoInfo.RepoOwner, _repoInfo.RepoName); 342 | tvWorkflows.ItemsSource = workflows.Workflows; 343 | } 344 | catch (ApiException ex) 345 | { 346 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Error retrieving Workflows: {ex.Message}:{ex.StatusCode}"); 347 | await ex.LogAsync(); 348 | } 349 | catch (Exception ex) 350 | { 351 | await ex.LogAsync(); 352 | } 353 | 354 | } 355 | private async Task RefreshSecretsAsync(GitHubClient client) 356 | { 357 | List secretList = new(); 358 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Loading Secrets..."); 359 | try 360 | { 361 | var repoSecrets = await client.Repository?.Actions?.Secrets?.GetAll(_repoInfo.RepoOwner, _repoInfo.RepoName); 362 | 363 | if (repoSecrets.TotalCount > 0) 364 | { 365 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Number of Repository Secrets found: {repoSecrets.TotalCount}"); 366 | tvSecrets.Header = $"{resx.HEADER_REPO_SECRETS} ({repoSecrets.TotalCount})"; 367 | foreach (var secret in repoSecrets.Secrets) 368 | { 369 | var updatedOrCreatedAt = secret.UpdatedAt.GetValueOrDefault(secret.CreatedAt); 370 | secretList.Add($"{secret.Name} ({updatedOrCreatedAt:g})"); 371 | } 372 | } 373 | else 374 | { 375 | tvSecrets.Header = resx.HEADER_REPO_SECRETS; 376 | secretList.Add(resx.NO_REPO_SECRETS); 377 | } 378 | } 379 | catch (ApiException ex) 380 | { 381 | if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized || ex.StatusCode == System.Net.HttpStatusCode.Forbidden) 382 | { 383 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Error retrieving Secrets: {ex.Message}:{ex.StatusCode}"); 384 | secretList.Add(resx.INSUFFICIENT_SECRET_PERMS); 385 | await ex.LogAsync(ex.Message); 386 | } 387 | } 388 | catch (Exception ex) 389 | { 390 | // check to see if a permission thing 391 | secretList.Add("Unable to retrieve Secrets, please check logs"); 392 | await ex.LogAsync(); 393 | } 394 | 395 | tvSecrets.ItemsSource = secretList; 396 | } 397 | 398 | private static GitHubClient GetGitHubClient() 399 | { 400 | var creds = CredentialManager.GetCredentials("git:https://github.com"); 401 | var client = new GitHubClient(new ProductHeaderValue("VisualStudio")) 402 | { 403 | Credentials = new(creds.Username, creds.Password) 404 | }; 405 | 406 | return client; 407 | } 408 | 409 | private void JobItem_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) 410 | { 411 | // get the items Tag 412 | if (sender is TreeViewItem item && item.Header is SimpleJob job && job.Url is not null) 413 | { 414 | Process.Start(job.Url); 415 | } 416 | } 417 | 418 | private void HandlePreviewMouseWheel(object sender, MouseWheelEventArgs e) 419 | { 420 | if (!e.Handled) 421 | { 422 | e.Handled = true; 423 | var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta); 424 | eventArg.RoutedEvent = UIElement.MouseWheelEvent; 425 | eventArg.Source = sender; 426 | var parent = ((Control)sender).Parent as UIElement; 427 | parent.RaiseEvent(eventArg); 428 | } 429 | } 430 | 431 | private async void AddSecret_Click(object sender, RoutedEventArgs e) 432 | { 433 | try 434 | { 435 | await UpsertRepositorySecret(string.Empty); 436 | } 437 | catch (ApiException ex) 438 | { 439 | if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized || ex.StatusCode == System.Net.HttpStatusCode.Forbidden) 440 | { 441 | await _pane.WriteLineAsync($"[{DateTime.UtcNow.ToString("o")}] Error saving Secret: {ex.Message}:{ex.StatusCode}"); 442 | await ex.LogAsync(ex.Message); 443 | } 444 | } 445 | catch (Exception ex) 446 | { 447 | await ex.LogAsync(); 448 | } 449 | } 450 | 451 | private async void EditSecret_Click(object sender, RoutedEventArgs e) 452 | { 453 | MenuItem menuItem = (MenuItem)sender; 454 | TextBlock tvi = GetParentTreeViewItem(menuItem); 455 | if (tvi is not null && tvi.Text.ToLowerInvariant().Contains(" (")) // yes a hack 456 | { 457 | string header = tvi.Text.ToString(); 458 | string secretName = header.Substring(0, header.IndexOf(" (")); 459 | await UpsertRepositorySecret(secretName); 460 | } 461 | } 462 | 463 | private TextBlock GetParentTreeViewItem(MenuItem menuItem) 464 | { 465 | var contextMenu = menuItem.CommandParameter as ContextMenu; 466 | if (contextMenu is not null) 467 | { 468 | var treeViewItem = contextMenu.PlacementTarget as TextBlock; 469 | if (treeViewItem is not null) 470 | { 471 | return treeViewItem; 472 | } 473 | } 474 | return null; 475 | } 476 | 477 | private async void DeleteSecret_Click(object sender, RoutedEventArgs e) 478 | { 479 | MenuItem menuItem = (MenuItem)sender; 480 | TextBlock tvi = GetParentTreeViewItem(menuItem); 481 | 482 | if (tvi is not null && tvi.Text.ToLowerInvariant().Contains(" (")) // yes a hack 483 | { 484 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 485 | // confirm the delete first 486 | int result = VsShellUtilities.ShowMessageBox(ServiceProvider.GlobalProvider, resx.CONFIRM_DELETE, resx.CONFIRM_DELETE_TITLE, Microsoft.VisualStudio.Shell.Interop.OLEMSGICON.OLEMSGICON_QUERY, Microsoft.VisualStudio.Shell.Interop.OLEMSGBUTTON.OLEMSGBUTTON_YESNOCANCEL, Microsoft.VisualStudio.Shell.Interop.OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_THIRD); 487 | 488 | var confirmResult = (MessageBoxResult)result; 489 | 490 | if (confirmResult == MessageBoxResult.Yes) 491 | { 492 | string header = tvi.Text.ToString(); 493 | string secretName = header.Substring(0, header.IndexOf(" (")); 494 | 495 | GitHubClient client = GetGitHubClient(); 496 | await client.Repository.Actions.Secrets.Delete(_repoInfo.RepoOwner, _repoInfo.RepoName, secretName); 497 | await RefreshSecretsAsync(client); 498 | } 499 | } 500 | } 501 | 502 | private async Task UpsertRepositorySecret(string secretName) 503 | { 504 | AddEditSecret addEditSecret = new AddEditSecret(secretName) 505 | { 506 | Owner = Application.Current.MainWindow 507 | }; 508 | bool? result = addEditSecret.ShowDialog(); 509 | if (result == true) 510 | { 511 | GitHubClient client = GetGitHubClient(); 512 | var pubKey = await client.Repository.Actions.Secrets.GetPublicKey(_repoInfo.RepoOwner, _repoInfo.RepoName); 513 | 514 | UpsertRepositorySecret encryptedSecret = new UpsertRepositorySecret(); 515 | if (pubKey != null) 516 | { 517 | var bytes = System.Text.Encoding.UTF8.GetBytes(addEditSecret.SecretValue); 518 | var key = Convert.FromBase64String(pubKey.Key); 519 | var sealedKeyBox = Sodium.SealedPublicKeyBox.Create(bytes, key); 520 | encryptedSecret.KeyId = pubKey.KeyId; 521 | encryptedSecret.EncryptedValue = Convert.ToBase64String(sealedKeyBox); 522 | _ = await client.Repository.Actions.Secrets.CreateOrUpdate(_repoInfo.RepoOwner, _repoInfo.RepoName, addEditSecret.SecretName, encryptedSecret); 523 | } 524 | await RefreshSecretsAsync(client); 525 | } 526 | } 527 | 528 | private void ViewLog_Click(object sender, RoutedEventArgs e) 529 | { 530 | MenuItem menuItem = (MenuItem)sender; 531 | TextBlock tvi = GetParentTreeViewItem(menuItem); 532 | 533 | // check the tag value to ensure it isn't null 534 | if (tvi is not null && tvi.Tag is not null) 535 | { 536 | string logUrl = tvi.Tag.ToString(); 537 | Process.Start(logUrl); 538 | } 539 | } 540 | 541 | private void RunWorkflow_Click(object sender, RoutedEventArgs e) 542 | { 543 | MenuItem menuItem = (MenuItem)sender; 544 | TextBlock tvi = GetParentTreeViewItem(menuItem); 545 | 546 | // check the tag value to ensure it isn't null 547 | if (tvi is not null && tvi.Tag is not null) 548 | { 549 | GitHubClient client = GetGitHubClient(); 550 | CreateWorkflowDispatch cwd = new CreateWorkflowDispatch(_repoInfo.CurrentBranch); 551 | 552 | try 553 | { 554 | _ = client.Actions.Workflows.CreateDispatch(_repoInfo.RepoOwner, _repoInfo.RepoName, (long)tvi.Tag, cwd); 555 | VS.StatusBar.ShowMessageAsync("Workflow run requested...").FireAndForget(); 556 | } 557 | catch (Exception ex) 558 | { 559 | Debug.WriteLine($"Failed to start workflow: {ex.Message}"); 560 | } 561 | } 562 | } 563 | 564 | private async void Secret_MouseDoubleClick(object sender, MouseButtonEventArgs e) 565 | { 566 | // get the items Tag 567 | if (sender is TreeViewItem item && item.Header is not null && item.Header.ToString().ToLowerInvariant().Contains(" (")) 568 | { 569 | string header = item.Header.ToString(); 570 | string secretName = header.Substring(0, header.IndexOf(" (")); 571 | 572 | if (secretName.ToLowerInvariant() != resx.NO_REPO_SECRETS.ToLowerInvariant() && secretName.ToLowerInvariant() != resx.HEADER_REPO_SECRETS.ToLowerInvariant()) 573 | { 574 | await UpsertRepositorySecret(secretName); 575 | e.Handled = true; 576 | } 577 | } 578 | } 579 | 580 | private void CancelRun_Click(object sender, RoutedEventArgs e) 581 | { 582 | MenuItem menuItem = (MenuItem)sender; 583 | TextBlock tvi = GetParentTreeViewItem(menuItem); 584 | 585 | // check the tag value to ensure it isn't null 586 | if (tvi is not null && tvi.DataContext is not null) 587 | { 588 | var run = tvi.DataContext as BaseWorkflowType; 589 | if (run is not null && run.Id is not null && !ConclusionFilter.IsFinished(run.Conclusion)) 590 | { 591 | GitHubClient client = GetGitHubClient(); 592 | _ = client.Actions.Workflows.Runs.Cancel(_repoInfo.RepoOwner, _repoInfo.RepoName, Int64.Parse(run.Id)); 593 | } 594 | } 595 | } 596 | } 597 | 598 | -------------------------------------------------------------------------------- /src/ToolWindows/MessageCommand.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubActionsVS.ToolWindows; 2 | 3 | public enum MessageCommand 4 | { 5 | GotoRepo, 6 | Refresh, 7 | OpenSettings, 8 | ReportFeedback 9 | } 10 | -------------------------------------------------------------------------------- /src/ToolWindows/MessagePayload.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubActionsVS.ToolWindows; 2 | 3 | public readonly record struct MessagePayload( 4 | MessageCommand Command, 5 | string Text = default); 6 | -------------------------------------------------------------------------------- /src/ToolWindows/ToolWindowMessenger.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubActionsVS.ToolWindows; 2 | public class ToolWindowMessenger 3 | { 4 | public void Send(MessagePayload payload) 5 | { 6 | MessageReceived?.Invoke(this, payload); 7 | } 8 | 9 | public event EventHandler MessageReceived; 10 | } 11 | -------------------------------------------------------------------------------- /src/UserControls/AddEditSecret.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |