├── .github └── workflows │ ├── build-preview.yml │ ├── build.yml │ ├── manual.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── README.md ├── [Content_Types].xml ├── media ├── actions-auto-complete.gif ├── allow-access.png ├── env-auto-complete.gif ├── github-auto-complete.gif ├── logs.gif ├── runs-on-auto-complete.gif └── workflow-auto-complete.gif ├── package-lock.json ├── package.json ├── resources ├── icons │ ├── dark │ │ ├── add.svg │ │ ├── circle-filled.svg │ │ ├── circle-outline.svg │ │ ├── conclusions │ │ │ ├── cancelled.svg │ │ │ ├── failure.svg │ │ │ ├── skip.svg │ │ │ └── success.svg │ │ ├── edit.svg │ │ ├── explorer.svg │ │ ├── logs.svg │ │ ├── refresh.svg │ │ ├── remove.svg │ │ ├── run.svg │ │ ├── runner-offline.svg │ │ ├── runner-online.svg │ │ └── statuses │ │ │ ├── in-progress.svg │ │ │ ├── queued.svg │ │ │ └── waiting.svg │ └── light │ │ ├── add.svg │ │ ├── circle-filled.svg │ │ ├── circle-outline.svg │ │ ├── conclusions │ │ ├── cancelled.svg │ │ ├── failure.svg │ │ ├── skip.svg │ │ └── success.svg │ │ ├── edit.svg │ │ ├── explorer.svg │ │ ├── logs.svg │ │ ├── refresh.svg │ │ ├── remove.svg │ │ ├── run.svg │ │ ├── runner-offline.svg │ │ ├── runner-online.svg │ │ └── statuses │ │ ├── in-progress.svg │ │ ├── queued.svg │ │ └── waiting.svg └── logo.png ├── src ├── api │ └── api.ts ├── auth │ └── auth.ts ├── client │ └── client.ts ├── commands │ ├── cancelWorkflowRun.ts │ ├── openWorkflowFile.ts │ ├── openWorkflowRun.ts │ ├── openWorkflowRunLogs.ts │ ├── orgLogin.ts │ ├── pinWorkflow.ts │ ├── rerunWorkflowRun.ts │ ├── secrets │ │ ├── addSecret.ts │ │ ├── copySecret.ts │ │ ├── deleteSecret.ts │ │ ├── manageOrgSecrets.ts │ │ └── updateSecret.ts │ ├── triggerWorkflowRun.ts │ └── unpinWorkflow.ts ├── configuration │ └── configuration.ts ├── extension.ts ├── external │ ├── README.md │ ├── protocol.ts │ └── ssh.ts ├── git │ └── repository.ts ├── log.ts ├── logs │ ├── ansi.ts │ ├── constants.ts │ ├── fileProvider.ts │ ├── foldingProvider.ts │ ├── formatProvider.ts │ ├── logInfoProvider.ts │ ├── model.ts │ ├── scheme.ts │ └── symbolProvider.ts ├── model.ts ├── pinnedWorkflows │ └── pinnedWorkflows.ts ├── secrets │ └── index.ts ├── tracker │ └── workflowDocumentTracker.ts ├── treeViews │ ├── current-branch │ │ ├── currentBranchRepoNode.ts │ │ └── noRunForBranchNode.ts │ ├── currentBranch.ts │ ├── icons.ts │ ├── settings.ts │ ├── settings │ │ ├── emptyEnvironmentSecretsNode.ts │ │ ├── environmentNode.ts │ │ ├── environmentSecretNode.ts │ │ ├── environmentsNode.ts │ │ ├── orgFeaturesNode.ts │ │ ├── orgSecretNode.ts │ │ ├── orgSecretsNode.ts │ │ ├── repoSecretNode.ts │ │ ├── repoSecretsNode.ts │ │ ├── secretsNode.ts │ │ ├── selfHostedRunnerNode.ts │ │ ├── selfHostedRunnersNode.ts │ │ ├── settingsRepoNode.ts │ │ └── types.ts │ ├── shared │ │ ├── authenticationNode.ts │ │ ├── errorNode.ts │ │ └── noGitHubRepositoryNode.ts │ ├── workflows.ts │ └── workflows │ │ ├── workflowJobNode.ts │ │ ├── workflowNode.ts │ │ ├── workflowRunNode.ts │ │ ├── workflowStepNode.ts │ │ └── workflowsRepoNode.ts ├── typings │ ├── git.d.ts │ └── ref.d.ts ├── utils │ └── array.ts └── workflow │ ├── diagnostics.ts │ └── workflow.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.github/workflows/build-preview.yml: -------------------------------------------------------------------------------- 1 | name: Build nightly release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2-beta 13 | with: 14 | node-version: '12' 15 | - run: rm package-lock.json 16 | - run: npm --no-git-tag-version version 1.0.${{ github.run_number }} 17 | - run: npm install 18 | - run: npm run package 19 | - name: Create Release 20 | id: create_release 21 | uses: actions/create-release@v1 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | tag_name: 'build-${{ github.run_number }}' 26 | release_name: Nightly Build ${{ github.run_number }} 27 | draft: false 28 | prerelease: false 29 | - name: Upload Release Asset 30 | id: upload-release-asset 31 | uses: actions/upload-release-asset@v1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | with: 35 | upload_url: ${{ steps.create_release.outputs.upload_url }} 36 | asset_path: ./vscode-github-actions-1.0.${{ github.run_number }}.vsix 37 | asset_name: vscode-github-actions-1.0.${{ github.run_number }}.vsix 38 | asset_content_type: application/vsix 39 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12.x 16 | - name: npm install, build 17 | run: | 18 | npm ci 19 | npm run build --if-present 20 | env: 21 | CI: true 22 | -------------------------------------------------------------------------------- /.github/workflows/manual.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow that is manually triggered 2 | 3 | name: Manual workflow 4 | 5 | # Controls when the action will run. Workflow runs when manually triggered using the UI 6 | # or API. 7 | on: 8 | workflow_dispatch: 9 | # Inputs the workflow accepts. 10 | inputs: 11 | name: 12 | # Friendly description to be shown in the UI instead of 'name' 13 | description: 'Person to greet' 14 | # Default value if no value is explicitly provided 15 | default: 'World' 16 | # Input has to be provided for the workflow to run 17 | required: true 18 | 19 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 20 | jobs: 21 | # This workflow contains a single job called "greet" 22 | greet: 23 | # The type of runner that the job will run on 24 | runs-on: ubuntu-latest 25 | 26 | # Steps represent a sequence of tasks that will be executed as part of the job 27 | steps: 28 | # Runs a single command using the runners shell 29 | - name: Send greeting 30 | run: echo "Hello ${{ github.event.inputs.name }}" 31 | - run: sleep 120 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release new version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | required: true 8 | description: 'Version to bump `package.json` to (format: x.y.z)' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - run: | 17 | git config --global user.email "cschleiden@live.de" 18 | git config --global user.name "Christopher Schleiden" 19 | - uses: actions/setup-node@v2 20 | with: 21 | node-version: '16' 22 | - run: npm version ${{ github.event.inputs.version }} 23 | - run: npm install 24 | - run: npm run package 25 | - name: Create Release 26 | id: create_release 27 | uses: actions/create-release@v1 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | with: 31 | tag_name: 'release-v${{ github.event.inputs.version }}' 32 | release_name: 'v${{ github.event.inputs.version }}' 33 | draft: false 34 | prerelease: false 35 | - name: Upload Release Asset 36 | id: upload-release-asset 37 | uses: actions/upload-release-asset@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | upload_url: ${{ steps.create_release.outputs.upload_url }} 42 | asset_path: ./vscode-github-actions-${{ github.event.inputs.version }}.vsix 43 | asset_name: vscode-github-actions-${{ github.event.inputs.version }}.vsix 44 | asset_content_type: application/vsix 45 | - name: Publish to marketplace 46 | uses: HaaLeo/publish-vscode-extension@v0 47 | with: 48 | pat: ${{ secrets.PUBLISHER_KEY }} 49 | registryUrl: https://marketplace.visualstudio.com 50 | extensionFile: ./vscode-github-actions-${{ github.event.inputs.version }}.vsix 51 | packagePath: '' 52 | - run: git push 53 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "47 2 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue is stale due to inactivity' 17 | stale-pr-message: 'This PR is stale due to inactivity' 18 | stale-issue-label: 'stale' 19 | stale-pr-label: 'stale' 20 | days-before-stale: 120 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | dist 4 | *.vsix 5 | .DS_Store 6 | runners 7 | .yalc -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Watch & Launch Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "--extensionDevelopmentPath=${workspaceFolder}", 10 | ], 11 | "skipFiles": [ 12 | "/**/*.js", 13 | "**/node_modules/**/*.js" 14 | ], 15 | "preLaunchTask": "npm: watch", 16 | "smartStep": true, 17 | "sourceMaps": true, 18 | "outFiles": [ 19 | "${workspaceFolder}/dist/*.js" 20 | ] 21 | }, 22 | { 23 | "name": "Run Web Extension in VS Code", 24 | "type": "pwa-extensionHost", 25 | "debugWebWorkerHost": true, 26 | "request": "launch", 27 | "args": [ 28 | "--extensionDevelopmentPath=${workspaceFolder}", 29 | "--extensionDevelopmentKind=web" 30 | ], 31 | "outFiles": ["${workspaceFolder}/dist/web/**/*.js"], 32 | "preLaunchTask": "npm: watch" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "github-actions.workflows.pinned.workflows": [".github/workflows/build.yml"] 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "group": "build", 10 | "isBackground": true, 11 | "problemMatcher": [ 12 | "$ts-webpack-watch" 13 | ], 14 | }, 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | out/ 4 | src/ 5 | tsconfig.json 6 | webpack.config.js 7 | runners 8 | media 9 | dist/*.map -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "vscode-github-actions" extension will be documented in this file. 4 | 5 | ## [v0.24.3] 6 | - Fix https://github.com/cschleiden/vscode-github-actions/issues/111 7 | 8 | ## [v0.24.0] 9 | - New "Current Branch" view 10 | 11 | ## [v0.23.0] 12 | - Various bug fixes and schema updates 13 | 14 | ## [v0.22.0] 15 | - Fix changes in repositories API 16 | 17 | ## [v0.21.2] 18 | - Remove duplicate trigger run button 19 | 20 | ## [v0.21.0] 21 | - Support github.dev web editor 22 | 23 | ## [v0.20.6] 24 | - Improve sign-in flow 25 | - Prepare for web execution 26 | 27 | ## [v0.20.3] 28 | - Revert `extensionKind` setting so extension works again in remote scenarios 29 | 30 | ## [v0.20.0] 31 | - Support multi-folder workspaces 32 | - Provide one-click commands for (un)pinning workflows 33 | - Updates for recent Actions workflow additions (`concurrency` etc.) 34 | - Basic support for environments and environment secrets 35 | 36 | ## [v0.17.0] 37 | 38 | - Support error background for pinned workflows - https://github.com/cschleiden/vscode-github-actions/issues/69 39 | ![](https://user-images.githubusercontent.com/2201819/107904773-9592ac00-6f01-11eb-89c6-7322a5912853.png) 40 | - Basic support for environment auto-completion 41 | 42 | ## [v0.15.0] 43 | 44 | - Support `include`/`exclude` for matrix strategies 45 | 46 | ## [v0.14.0] 47 | 48 | - Consume updated parser 49 | - Fixes issues with `!` in expressions 50 | - Fixes issues with using `step..outputs` in expressions 51 | 52 | ## [v0.13.0] 53 | 54 | - Fixed: https://github.com/cschleiden/vscode-github-actions/issues/42 55 | 56 | ## [v0.12.0] 57 | 58 | - Various bugfixes for expression validation 59 | - Added missing `pull_request_target` event 60 | - Improved error reporting for unknown keys 61 | - Bugfix: show "Run workflow" in context menu when workflow has _only_ `workflow_dispatch`, do not require also `repository_dispatch` 62 | 63 | ## [v0.11.0] 64 | 65 | - Basic support for `environment` in jobs 66 | 67 | ## [v0.10.0] 68 | 69 | - Fixes error when trying to open expired logs (#19) 70 | - Removed login command, authorization is now handled via the GitHub authentication provider (#50) 71 | - Fixes error where extension can not be enabled/disabled per workspace (#50) 72 | - Support for validating `workflow_dispatch` events 73 | - Support for triggering `workflow_dispatch` workflows 74 | 75 | ## [v0.9.0] 76 | - Updated `github-actions-parser` dependency to fix a number of auto-complete and validation bugs 77 | - Removed edit preview features, they are now enabled by default 78 | - Changed the scope of the org features setting, so that it can be set for remote workspaces, too 79 | 80 | ## [v0.8.1] 81 | - Fixes expression auto-completion in YAML multi-line strings 82 | 83 | ## [v0.8.0] The one with the auto-completion 84 | - Enable the `github-actions.preview-features` setting to start using smart auto-complete and validation. 85 | 86 | ## [v0.7.0] 87 | - Support the VS Code authentication API with the VS Code July release (1.48). This replaces the previous, manual PAT-based authentication flow. 88 | 89 | Note: Organization features like displaying Organization-level Secrets require the `admin:org` scope. Since not everyone might want to give this scope to the VS Code token, by default this scope is not requested. There is a new setting to request the scope. 90 | 91 | - Moved all commands into a `github-actions.` namespace and "GitHub Actions" category in the command palette 92 | 93 | ## [v0.6.0] 94 | - Update success icon 95 | - Support org secrets 96 | 97 | ## [v0.5.1] 98 | - Roll back VS Code authentication API change for now. Wait until it becomes stable to remove need for manual enabling. 99 | 100 | ## [v0.5.0] 101 | - Support the VS Code authentication API. This replaces the previous, manual PAT-based authentication flow. 102 | 103 | ## [v0.4.1] 104 | - Add inline run button to workflow list 105 | 106 | ## [v0.4.0] 107 | - Bugfixes for remote development 108 | - Show run button for workflows using `repository_dispatch` in editor title bar 109 | 110 | ## [v0.3.0] 111 | - Enable pinning workflows 112 | 113 | ## [v0.2.0] 114 | - Bugfix for displaying self-hosted runners 115 | 116 | ## [v0.1.16] 117 | - Bugfix: "Trigger workflow" shows up for workflows using the object notation to define the `on` events 118 | - Feature: If `repository_dispatch` is filtered to specific types, the trigger workflow run will be pre-populated with those. 119 | 120 | ## [v0.1.15] 121 | - Support colored logs 122 | 123 | ## [v0.1.14] 124 | - Support displaying logs for workflows 125 | 126 | ## [v0.1.1] 127 | - Initial prototype 128 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cschleiden 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Christopher Schleiden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2023-03-28: The extension is now an [official part](https://github.blog/2023-03-28-announcing-the-github-actions-extension-for-vs-code/) of the product 🎉 2 | 3 | 4 | Get it here: https://marketplace.visualstudio.com/items?itemName=github.vscode-github-actions 5 | 6 | 7 | Find the latest code: https://github.com/github/vscode-github-actions 8 | 9 | 10 | 11 | ---- 12 | 13 | # GitHub Actions for VS Code 14 | 15 | [![Build](https://github.com/cschleiden/vscode-github-actions/actions/workflows/build.yml/badge.svg)](https://github.com/cschleiden/vscode-github-actions/actions/workflows/build.yml) 16 | 17 | Simple extension to interact with GitHub Actions from within VS Code. 18 | 19 | ## Setup 20 | 21 | 1. Install the extension from the [Marketplace](https://marketplace.visualstudio.com/items?itemName=cschleiden.vscode-github-actions) 22 | 2. Open a repository with a `github.com` `origin` git remote 23 | 3. When prompted, allow `GitHub Actions` access to your GitHub account: 24 | 25 | ![Sign in via Accounts menu](./media/allow-access.png) 26 | 27 | ## Features 28 | 29 | The extension provides a convenient way to monitor workflows and workflow runs from VS Code as well as language-server features for editing workflow YAML files. 30 | 31 | ### Auto-complete and documentation 32 | 33 | No additional YAML extension needed, this extension includes a built-in language server with support for the workflow schema: 34 | 35 | ![Workflow auto-complete](./media/workflow-auto-complete.gif) 36 | 37 | Auto-completion and validation for every action you reference in `uses`: 38 | 39 | ![Actions auto-complete](./media/actions-auto-complete.gif) 40 | 41 | Auto-completion and validation of labels for hosted and self-hosted runners: 42 | 43 | ![Auto-complete runner label](./media/runs-on-auto-complete.gif) 44 | 45 | #### Expressions auto-complete 46 | 47 | Auto-completion, validation, and evaluation of expressions: 48 | 49 | ![Auto-complete and evaluation of expressions](./media/env-auto-complete.gif) 50 | 51 | Auto-complete and validate all webhook event payloads: 52 | 53 | ![Auto-complete github event expressions](./media/github-auto-complete.gif) 54 | 55 | ### Monitor workflow runs 56 | 57 | See runs for workflows in the repository, drill into jobs and steps, and inspect logs: 58 | 59 | ![See workflows and runs for the current repository](./media/logs.gif) 60 | 61 | 62 | ### Other features 63 | 64 | - Trigger `repository_dispatch` or `workflow_dispatch` workflow runs 65 | - View registered self-hosted runners and environments for the current repository 66 | - View, edit, and add secrets 67 | - Pin workflow to the VS Code status bar 68 | -------------------------------------------------------------------------------- /[Content_Types].xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /media/actions-auto-complete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cschleiden/vscode-github-actions/a49010e72e800be1fc31ae03822e2ecb8479a1a3/media/actions-auto-complete.gif -------------------------------------------------------------------------------- /media/allow-access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cschleiden/vscode-github-actions/a49010e72e800be1fc31ae03822e2ecb8479a1a3/media/allow-access.png -------------------------------------------------------------------------------- /media/env-auto-complete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cschleiden/vscode-github-actions/a49010e72e800be1fc31ae03822e2ecb8479a1a3/media/env-auto-complete.gif -------------------------------------------------------------------------------- /media/github-auto-complete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cschleiden/vscode-github-actions/a49010e72e800be1fc31ae03822e2ecb8479a1a3/media/github-auto-complete.gif -------------------------------------------------------------------------------- /media/logs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cschleiden/vscode-github-actions/a49010e72e800be1fc31ae03822e2ecb8479a1a3/media/logs.gif -------------------------------------------------------------------------------- /media/runs-on-auto-complete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cschleiden/vscode-github-actions/a49010e72e800be1fc31ae03822e2ecb8479a1a3/media/runs-on-auto-complete.gif -------------------------------------------------------------------------------- /media/workflow-auto-complete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cschleiden/vscode-github-actions/a49010e72e800be1fc31ae03822e2ecb8479a1a3/media/workflow-auto-complete.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-github-actions", 3 | "displayName": "GitHub Actions", 4 | "publisher": "cschleiden", 5 | "icon": "resources/logo.png", 6 | "license": "MIT", 7 | "repository": { 8 | "url": "https://github.com/cschleiden/vscode-github-actions" 9 | }, 10 | "description": "GitHub Actions workflows and runs for github.com hosted repositories in VS Code", 11 | "version": "0.24.3", 12 | "engines": { 13 | "vscode": "^1.60.0" 14 | }, 15 | "extensionKind": [ 16 | "workspace" 17 | ], 18 | "capabilities": { 19 | "virtualWorkspaces": true 20 | }, 21 | "categories": [ 22 | "Other" 23 | ], 24 | "activationEvents": [ 25 | "onView:workflows", 26 | "onView:settings", 27 | "workspaceContains:**/.github/workflows/**" 28 | ], 29 | "main": "./dist/extension-node.js", 30 | "browser": "./dist/extension-web.js", 31 | "contributes": { 32 | "configuration": { 33 | "title": "GitHub Actions", 34 | "properties": { 35 | "github-actions.workflows.pinned.workflows": { 36 | "description": "Workflows to show in the status bar, identified by their paths", 37 | "type": "array", 38 | "scope": "window" 39 | }, 40 | "github-actions.workflows.pinned.refresh.enabled": { 41 | "type": "boolean", 42 | "description": "Auto-refresh pinned workflows. Note: this uses polling and counts against your GitHub API rate limit", 43 | "default": false, 44 | "scope": "window" 45 | }, 46 | "github-actions.workflows.pinned.refresh.interval": { 47 | "type": "number", 48 | "description": "Time to wait between calls to update pinned workflows in seconds", 49 | "default": 30, 50 | "scope": "window" 51 | }, 52 | "github-actions.org-features": { 53 | "type": "boolean", 54 | "description": "Whether org features should be enabled. Requires the `admin:org` scope", 55 | "default": false, 56 | "scope": "window" 57 | }, 58 | "github-actions.remote-name": { 59 | "type": "string", 60 | "description": "The name of the repository's git remote that points to GitHub", 61 | "default": "origin", 62 | "scope": "window" 63 | } 64 | } 65 | }, 66 | "commands": [ 67 | { 68 | "command": "github-actions.explorer.refresh", 69 | "category": "GitHub Actions", 70 | "title": "Refresh", 71 | "icon": { 72 | "dark": "resources/icons/dark/refresh.svg", 73 | "light": "resources/icons/light/refresh.svg" 74 | } 75 | }, 76 | { 77 | "command": "github-actions.explorer.current-branch.refresh", 78 | "category": "GitHub Actions", 79 | "title": "Refresh current branch", 80 | "icon": { 81 | "dark": "resources/icons/dark/refresh.svg", 82 | "light": "resources/icons/light/refresh.svg" 83 | } 84 | }, 85 | { 86 | "command": "github-actions.explorer.openRun", 87 | "category": "GitHub Actions", 88 | "title": "Open workflow run", 89 | "when": "viewItem =~ /workflow/" 90 | }, 91 | { 92 | "command": "github-actions.explorer.openWorkflowFile", 93 | "category": "GitHub Actions", 94 | "title": "Open workflow", 95 | "when": "viewItem =~ /workflow/" 96 | }, 97 | { 98 | "command": "github-actions.explorer.triggerRun", 99 | "category": "GitHub Actions", 100 | "title": "Trigger workflow", 101 | "when": "viewItem =~ /workflow/ && viewItem =~ /dispatch/", 102 | "icon": { 103 | "dark": "resources/icons/dark/run.svg", 104 | "light": "resources/icons/light/run.svg" 105 | } 106 | }, 107 | { 108 | "command": "github-actions.workflow.run.open", 109 | "category": "GitHub Actions", 110 | "title": "Open workflow run", 111 | "when": "viewItem =~ /run/" 112 | }, 113 | { 114 | "command": "github-actions.workflow.logs", 115 | "category": "GitHub Actions", 116 | "title": "View logs", 117 | "when": "viewItem =~ /job/ || viewItem =~ /step/", 118 | "icon": { 119 | "dark": "resources/icons/dark/logs.svg", 120 | "light": "resources/icons/light/logs.svg" 121 | } 122 | }, 123 | { 124 | "command": "github-actions.workflow.run.rerun", 125 | "category": "GitHub Actions", 126 | "title": "Rerun workflow run", 127 | "when": "viewItem =~ /run/ && viewItem =~ /rerunnable/" 128 | }, 129 | { 130 | "command": "github-actions.workflow.run.cancel", 131 | "category": "GitHub Actions", 132 | "title": "Cancel workflow run", 133 | "when": "viewItem =~ /run/ && viewItem =~ /cancelable/" 134 | }, 135 | { 136 | "command": "github-actions.settings.secrets.manage", 137 | "category": "GitHub Actions", 138 | "title": "Add new secret", 139 | "icon": "$(globe)" 140 | }, 141 | { 142 | "command": "github-actions.settings.secret.add", 143 | "category": "GitHub Actions", 144 | "title": "Add new secret", 145 | "icon": { 146 | "dark": "resources/icons/dark/add.svg", 147 | "light": "resources/icons/light/add.svg" 148 | } 149 | }, 150 | { 151 | "command": "github-actions.settings.secret.copy", 152 | "category": "GitHub Actions", 153 | "title": "Copy secret name" 154 | }, 155 | { 156 | "command": "github-actions.settings.secret.update", 157 | "category": "GitHub Actions", 158 | "title": "Update secret", 159 | "icon": { 160 | "dark": "resources/icons/dark/edit.svg", 161 | "light": "resources/icons/light/edit.svg" 162 | } 163 | }, 164 | { 165 | "command": "github-actions.settings.secret.delete", 166 | "category": "GitHub Actions", 167 | "title": "Delete secret", 168 | "icon": { 169 | "dark": "resources/icons/dark/remove.svg", 170 | "light": "resources/icons/light/remove.svg" 171 | } 172 | }, 173 | { 174 | "command": "github-actions.workflow.pin", 175 | "category": "GitHub Actions", 176 | "title": "Pin workflow", 177 | "icon": "$(pin)" 178 | }, 179 | { 180 | "command": "github-actions.workflow.unpin", 181 | "category": "GitHub Actions", 182 | "title": "Unpin workflow", 183 | "icon": "$(pinned)" 184 | }, 185 | { 186 | "command": "github-actions.workflow.run.open", 187 | "title": "Open in browser", 188 | "icon": "$(globe)" 189 | } 190 | ], 191 | "views": { 192 | "github-actions": [ 193 | { 194 | "id": "github-actions.current-branch", 195 | "name": "Current Branch" 196 | }, 197 | { 198 | "id": "github-actions.workflows", 199 | "name": "Workflows" 200 | }, 201 | { 202 | "id": "github-actions.settings", 203 | "name": "Settings" 204 | } 205 | ] 206 | }, 207 | "viewsContainers": { 208 | "activitybar": [ 209 | { 210 | "id": "github-actions", 211 | "title": "GitHub Actions", 212 | "icon": "resources/icons/light/explorer.svg" 213 | } 214 | ] 215 | }, 216 | "menus": { 217 | "view/title": [ 218 | { 219 | "command": "github-actions.explorer.refresh", 220 | "group": "navigation", 221 | "when": "view == github-actions.workflows || view == github-actions.settings" 222 | }, 223 | { 224 | "command": "github-actions.explorer.current-branch.refresh", 225 | "group": "navigation", 226 | "when": "view == github-actions.current-branch" 227 | } 228 | ], 229 | "editor/title": [ 230 | { 231 | "command": "github-actions.explorer.triggerRun", 232 | "when": "(githubActions:activeFile =~ /rdispatch/ || githubActions:activeFile =~ /wdispatch/) && resourceExtname =~ /\\.ya?ml/", 233 | "group": "navigation" 234 | } 235 | ], 236 | "view/item/context": [ 237 | { 238 | "command": "github-actions.explorer.openWorkflowFile", 239 | "when": "viewItem =~ /workflow/" 240 | }, 241 | { 242 | "command": "github-actions.workflow.pin", 243 | "group": "inline@1", 244 | "when": "viewItem =~ /workflow/ && viewItem =~ /pinnable/" 245 | }, 246 | { 247 | "command": "github-actions.workflow.pin", 248 | "when": "viewItem =~ /workflow/ && viewItem =~ /pinnable/" 249 | }, 250 | { 251 | "command": "github-actions.workflow.unpin", 252 | "group": "inline@2", 253 | "when": "viewItem =~ /workflow/ && viewItem =~ /pinned/" 254 | }, 255 | { 256 | "command": "github-actions.workflow.unpin", 257 | "when": "viewItem =~ /workflow/ && viewItem =~ /pinned/" 258 | }, 259 | { 260 | "command": "github-actions.explorer.triggerRun", 261 | "group": "inline@10", 262 | "when": "viewItem =~ /rdispatch/ || viewItem =~ /wdispatch/" 263 | }, 264 | { 265 | "command": "github-actions.explorer.triggerRun", 266 | "when": "viewItem =~ /rdispatch/ || viewItem =~ /wdispatch/" 267 | }, 268 | { 269 | "command": "github-actions.workflow.run.open", 270 | "when": "viewItem =~ /run/" 271 | }, 272 | { 273 | "command": "github-actions.workflow.logs", 274 | "group": "inline", 275 | "when": "viewItem =~ /job/ && viewItem =~ /completed/" 276 | }, 277 | { 278 | "command": "github-actions.workflow.run.cancel", 279 | "when": "viewItem =~ /run/ && viewItem =~ /cancelable/" 280 | }, 281 | { 282 | "command": "github-actions.workflow.run.rerun", 283 | "when": "viewItem =~ /run/ && viewItem =~ /rerunnable/" 284 | }, 285 | { 286 | "command": "github-actions.settings.secret.add", 287 | "group": "inline", 288 | "when": "viewItem == 'secrets'" 289 | }, 290 | { 291 | "command": "github-actions.settings.secrets.manage", 292 | "group": "inline", 293 | "when": "viewItem == 'org-secrets'" 294 | }, 295 | { 296 | "command": "github-actions.settings.secret.update", 297 | "when": "viewItem == 'secret'", 298 | "group": "inline@1" 299 | }, 300 | { 301 | "command": "github-actions.settings.secret.copy", 302 | "when": "viewItem == 'secret' || viewItem == 'org-secret'", 303 | "group": "context" 304 | }, 305 | { 306 | "command": "github-actions.settings.secret.delete", 307 | "when": "viewItem == 'secret'", 308 | "group": "inline@2" 309 | }, 310 | { 311 | "command": "github-actions.workflow.run.open", 312 | "when": "viewItem =~ /run/", 313 | "group": "inline" 314 | } 315 | ], 316 | "commandPalette": [ 317 | { 318 | "command": "github-actions.explorer.openWorkflowFile", 319 | "when": "false" 320 | }, 321 | { 322 | "command": "github-actions.explorer.triggerRun", 323 | "when": "false" 324 | }, 325 | { 326 | "command": "github-actions.workflow.run.open", 327 | "when": "false" 328 | }, 329 | { 330 | "command": "github-actions.workflow.run.cancel", 331 | "when": "false" 332 | }, 333 | { 334 | "command": "github-actions.workflow.run.rerun", 335 | "when": "false" 336 | }, 337 | { 338 | "command": "github-actions.settings.secrets.manage", 339 | "when": "false" 340 | }, 341 | { 342 | "command": "github-actions.settings.secret.add", 343 | "when": "false" 344 | }, 345 | { 346 | "command": "github-actions.settings.secret.update", 347 | "when": "false" 348 | }, 349 | { 350 | "command": "github-actions.settings.secret.delete", 351 | "when": "false" 352 | }, 353 | { 354 | "command": "github-actions.workflow.pin", 355 | "when": "false" 356 | }, 357 | { 358 | "command": "github-actions.workflow.unpin", 359 | "when": "false" 360 | } 361 | ] 362 | } 363 | }, 364 | "scripts": { 365 | "clean": "rimraf ./dist ./out", 366 | "package": "npm run clean && vsce package", 367 | "build": "webpack --mode production", 368 | "vscode:prepublish": "npm run build", 369 | "watch": "webpack --watch --mode development --env esbuild --info-verbosity verbose", 370 | "open-in-browser": "vscode-test-web --extensionDevelopmentPath=. ." 371 | }, 372 | "devDependencies": { 373 | "@types/js-yaml": "^3.12.1", 374 | "@types/node": "14.14.32", 375 | "@types/uuid": "^3.4.6", 376 | "@types/vscode": "1.60.0", 377 | "@types/yaml": "^1.2.0", 378 | "@vscode/test-web": "*", 379 | "buffer": "^6.0.3", 380 | "node-loader": "^0.6.0", 381 | "path-browserify": "^1.0.1", 382 | "rimraf": "^3.0.1", 383 | "ts-loader": "^6.2.1", 384 | "tslint": "^5.20.0", 385 | "typescript": "4.2.3", 386 | "vsce": "^2.13.0", 387 | "webpack": "^5.74.0", 388 | "webpack-cli": "^4.10.0" 389 | }, 390 | "dependencies": { 391 | "@octokit/rest": "18.6.7", 392 | "abab": "^2.0.5", 393 | "github-actions-parser": "0.27.0", 394 | "js-yaml": "^3.14.0", 395 | "ssh-config": "^3.0.0", 396 | "tunnel": "0.0.6", 397 | "tweetsodium": "0.0.4", 398 | "util": "^0.12.1", 399 | "uuid": "^3.3.3", 400 | "yaml": "^1.7.2" 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /resources/icons/dark/add.svg: -------------------------------------------------------------------------------- 1 | Layer 1 -------------------------------------------------------------------------------- /resources/icons/dark/circle-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/dark/circle-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/dark/conclusions/cancelled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/dark/conclusions/failure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/dark/conclusions/skip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/dark/conclusions/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icons/dark/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/dark/explorer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/dark/logs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/dark/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icons/dark/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/dark/run.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /resources/icons/dark/runner-offline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icons/dark/runner-online.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icons/dark/statuses/in-progress.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/dark/statuses/queued.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/dark/statuses/waiting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/light/add.svg: -------------------------------------------------------------------------------- 1 | Layer 1 -------------------------------------------------------------------------------- /resources/icons/light/circle-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /resources/icons/light/circle-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/light/conclusions/cancelled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/light/conclusions/failure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/light/conclusions/skip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/light/conclusions/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icons/light/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/light/explorer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icons/light/logs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/light/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icons/light/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/light/run.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /resources/icons/light/runner-offline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icons/light/runner-online.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/icons/light/statuses/in-progress.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/light/statuses/queued.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icons/light/statuses/waiting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cschleiden/vscode-github-actions/a49010e72e800be1fc31ae03822e2ecb8479a1a3/resources/logo.png -------------------------------------------------------------------------------- /src/api/api.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from "@octokit/rest"; 2 | 3 | export function getClient(token: string): Octokit { 4 | return new Octokit({ 5 | auth: token, 6 | userAgent: "VS Code GitHub Actions", 7 | previews: ["jane-hopper"], 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/auth/auth.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { 4 | orgFeaturesEnabled, 5 | updateOrgFeaturesEnabled 6 | } from "../configuration/configuration"; 7 | 8 | import { resetGitHubContext } from "../git/repository"; 9 | 10 | const AUTH_PROVIDER_ID = "github"; 11 | const DEFAULT_SCOPES = ["repo", "workflow"]; 12 | const ORG_SCOPES = [...DEFAULT_SCOPES, "admin:org"]; 13 | 14 | export function registerListeners(context: vscode.ExtensionContext): void { 15 | context.subscriptions.push( 16 | vscode.authentication.onDidChangeSessions(async (e) => { 17 | if (e.provider.id === AUTH_PROVIDER_ID) { 18 | await enableOrgFeatures(); 19 | } 20 | }) 21 | ); 22 | } 23 | 24 | export async function getSession(): Promise { 25 | const existingSession = await vscode.authentication.getSession( 26 | AUTH_PROVIDER_ID, 27 | getScopes(), 28 | { 29 | createIfNone: true, 30 | } 31 | ); 32 | 33 | if (!existingSession) { 34 | throw new Error( 35 | "Could not get token from the GitHub authentication provider. \nPlease sign-in and allow access." 36 | ); 37 | } 38 | 39 | return existingSession; 40 | } 41 | 42 | export async function enableOrgFeatures() { 43 | await updateOrgFeaturesEnabled(true); 44 | 45 | await resetGitHubContext(); 46 | 47 | // TODO: CS: There has be a better way :) 48 | await vscode.commands.executeCommand("github-actions.explorer.refresh"); 49 | } 50 | 51 | function getScopes(): string[] { 52 | if (orgFeaturesEnabled()) { 53 | return ORG_SCOPES; 54 | } 55 | 56 | return DEFAULT_SCOPES; 57 | } 58 | -------------------------------------------------------------------------------- /src/client/client.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from "@octokit/rest"; 2 | import { getClient as getAPIClient } from "../api/api"; 3 | 4 | export async function getClient(token: string): Promise { 5 | return getAPIClient(token); 6 | } 7 | -------------------------------------------------------------------------------- /src/commands/cancelWorkflowRun.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../git/repository"; 3 | import { WorkflowRun } from "../model"; 4 | 5 | interface CancelWorkflowRunLogsCommandArgs { 6 | gitHubRepoContext: GitHubRepoContext; 7 | run: WorkflowRun; 8 | } 9 | 10 | export function registerCancelWorkflowRun(context: vscode.ExtensionContext) { 11 | context.subscriptions.push( 12 | vscode.commands.registerCommand( 13 | "github-actions.workflow.run.cancel", 14 | async (args: CancelWorkflowRunLogsCommandArgs) => { 15 | const gitHubContext = args.gitHubRepoContext; 16 | const run = args.run; 17 | 18 | try { 19 | await gitHubContext.client.actions.cancelWorkflowRun({ 20 | owner: gitHubContext.owner, 21 | repo: gitHubContext.name, 22 | run_id: run.id, 23 | }); 24 | } catch (e: any) { 25 | vscode.window.showErrorMessage( 26 | `Could not cancel workflow: '${e.message}'` 27 | ); 28 | } 29 | 30 | vscode.commands.executeCommand("github-actions.explorer.refresh"); 31 | } 32 | ) 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/openWorkflowFile.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { GitHubRepoContext } from "../git/repository"; 4 | import { Workflow } from "../model"; 5 | import { getWorkflowUri } from "../workflow/workflow"; 6 | 7 | interface OpenWorkflowCommandArgs { 8 | gitHubRepoContext: GitHubRepoContext; 9 | wf: Workflow; 10 | } 11 | 12 | export function registerOpenWorkflowFile(context: vscode.ExtensionContext) { 13 | context.subscriptions.push( 14 | vscode.commands.registerCommand( 15 | "github-actions.explorer.openWorkflowFile", 16 | async (args: OpenWorkflowCommandArgs) => { 17 | const { wf, gitHubRepoContext } = args; 18 | 19 | const fileUri = getWorkflowUri(gitHubRepoContext, wf.path); 20 | if (fileUri) { 21 | const textDocument = await vscode.workspace.openTextDocument(fileUri); 22 | vscode.window.showTextDocument(textDocument); 23 | return; 24 | } 25 | 26 | // File not found in workspace 27 | vscode.window.showErrorMessage( 28 | `Workflow ${wf.path} not found in current workspace` 29 | ); 30 | } 31 | ) 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/openWorkflowRun.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { WorkflowRun } from "../model"; 3 | 4 | export function registerOpenWorkflowRun(context: vscode.ExtensionContext) { 5 | context.subscriptions.push( 6 | vscode.commands.registerCommand( 7 | "github-actions.workflow.run.open", 8 | async (args) => { 9 | const run: WorkflowRun = args.run; 10 | const url = run.html_url; 11 | vscode.env.openExternal(vscode.Uri.parse(url)); 12 | } 13 | ) 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/openWorkflowRunLogs.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../git/repository"; 3 | import { updateDecorations } from "../logs/formatProvider"; 4 | import { getLogInfo } from "../logs/logInfoProvider"; 5 | import { buildLogURI } from "../logs/scheme"; 6 | import { WorkflowJob, WorkflowStep } from "../model"; 7 | 8 | export interface OpenWorkflowRunLogsCommandArgs { 9 | gitHubRepoContext: GitHubRepoContext; 10 | job: WorkflowJob; 11 | step?: WorkflowStep; 12 | } 13 | 14 | export function registerOpenWorkflowRunLogs(context: vscode.ExtensionContext) { 15 | context.subscriptions.push( 16 | vscode.commands.registerCommand( 17 | "github-actions.workflow.logs", 18 | async (args: OpenWorkflowRunLogsCommandArgs) => { 19 | const gitHubRepoContext = args.gitHubRepoContext; 20 | const job = args.job; 21 | const step = args.step; 22 | const uri = buildLogURI( 23 | `%23${job.run_id} - ${job.name}`, 24 | gitHubRepoContext.owner, 25 | gitHubRepoContext.name, 26 | job.id 27 | ); 28 | 29 | const doc = await vscode.workspace.openTextDocument(uri); 30 | const editor = await vscode.window.showTextDocument(doc, { 31 | preview: false, 32 | }); 33 | 34 | const logInfo = getLogInfo(uri); 35 | if (!logInfo) { 36 | throw new Error("Could not get log info"); 37 | } 38 | 39 | // Custom formatting after the editor has been opened 40 | updateDecorations(editor, logInfo); 41 | 42 | // Deep linking to steps 43 | if (step) { 44 | let matchingSection = logInfo.sections.find( 45 | (s) => s.name && s.name === step.name 46 | ); 47 | if (!matchingSection) { 48 | // If we cannot match by name, see if we can try to match by number 49 | matchingSection = logInfo.sections[step.number - 1]; 50 | } 51 | 52 | if (matchingSection) { 53 | editor.revealRange( 54 | new vscode.Range( 55 | matchingSection.start, 56 | 0, 57 | matchingSection.start, 58 | 0 59 | ), 60 | vscode.TextEditorRevealType.InCenter 61 | ); 62 | } 63 | } 64 | } 65 | ) 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/commands/orgLogin.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { enableOrgFeatures } from "../auth/auth"; 4 | 5 | export function registerOrgLogin(context: vscode.ExtensionContext) { 6 | context.subscriptions.push( 7 | vscode.commands.registerCommand( 8 | "github-actions.auth.org-login", 9 | async () => { 10 | enableOrgFeatures(); 11 | } 12 | ) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/pinWorkflow.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { GitHubRepoContext } from "../git/repository"; 4 | import { Workflow } from "../model"; 5 | import { getWorkflowUri } from "../workflow/workflow"; 6 | import { pinWorkflow } from "../configuration/configuration"; 7 | 8 | interface PinWorkflowCommandOptions { 9 | gitHubRepoContext: GitHubRepoContext; 10 | wf?: Workflow; 11 | 12 | updateContextValue(): void; 13 | } 14 | 15 | export function registerPinWorkflow(context: vscode.ExtensionContext) { 16 | context.subscriptions.push( 17 | vscode.commands.registerCommand( 18 | "github-actions.workflow.pin", 19 | async (args: PinWorkflowCommandOptions) => { 20 | const { gitHubRepoContext, wf } = args; 21 | 22 | if (!wf) { 23 | return; 24 | } 25 | 26 | const workflowFullPath = getWorkflowUri(gitHubRepoContext, wf.path); 27 | if (!workflowFullPath) { 28 | return; 29 | } 30 | 31 | const relativeWorkflowPath = 32 | vscode.workspace.asRelativePath(workflowFullPath); 33 | await pinWorkflow(relativeWorkflowPath); 34 | 35 | args.updateContextValue(); 36 | vscode.commands.executeCommand("github-actions.explorer.refresh"); 37 | } 38 | ) 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/rerunWorkflowRun.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { GitHubRepoContext } from "../git/repository"; 4 | import { WorkflowRun } from "../model"; 5 | 6 | interface ReRunWorkflowRunLogsCommandArgs { 7 | gitHubRepoContext: GitHubRepoContext; 8 | run: WorkflowRun; 9 | } 10 | 11 | export function registerReRunWorkflowRun(context: vscode.ExtensionContext) { 12 | context.subscriptions.push( 13 | vscode.commands.registerCommand( 14 | "github-actions.workflow.run.rerun", 15 | async (args: ReRunWorkflowRunLogsCommandArgs) => { 16 | const gitHubContext = args.gitHubRepoContext; 17 | const run = args.run; 18 | 19 | try { 20 | await gitHubContext.client.actions.reRunWorkflow({ 21 | owner: gitHubContext.owner, 22 | repo: gitHubContext.name, 23 | run_id: run.id, 24 | }); 25 | } catch (e: any) { 26 | vscode.window.showErrorMessage( 27 | `Could not rerun workflow: '${e.message}'` 28 | ); 29 | } 30 | 31 | vscode.commands.executeCommand("github-actions.explorer.refresh"); 32 | } 33 | ) 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/secrets/addSecret.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { encodeSecret } from "../../secrets"; 4 | 5 | interface AddSecretCommandArgs { 6 | gitHubRepoContext: GitHubRepoContext; 7 | } 8 | 9 | export function registerAddSecret(context: vscode.ExtensionContext) { 10 | context.subscriptions.push( 11 | vscode.commands.registerCommand( 12 | "github-actions.settings.secret.add", 13 | async (args: AddSecretCommandArgs) => { 14 | const gitHubContext = args.gitHubRepoContext; 15 | 16 | const name = await vscode.window.showInputBox({ 17 | prompt: "Enter name for new secret", 18 | }); 19 | 20 | if (!name) { 21 | return; 22 | } 23 | 24 | const value = await vscode.window.showInputBox({ 25 | prompt: "Enter the new secret value", 26 | }); 27 | 28 | if (value) { 29 | try { 30 | const keyResponse = 31 | await gitHubContext.client.actions.getRepoPublicKey({ 32 | owner: gitHubContext.owner, 33 | repo: gitHubContext.name, 34 | }); 35 | 36 | const key_id = keyResponse.data.key_id; 37 | const key = keyResponse.data.key; 38 | 39 | await gitHubContext.client.actions.createOrUpdateRepoSecret({ 40 | owner: gitHubContext.owner, 41 | repo: gitHubContext.name, 42 | secret_name: name, 43 | key_id: key_id, 44 | encrypted_value: encodeSecret(key, value), 45 | }); 46 | } catch (e: any) { 47 | vscode.window.showErrorMessage(e.message); 48 | } 49 | } 50 | 51 | vscode.commands.executeCommand("github-actions.explorer.refresh"); 52 | } 53 | ) 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/commands/secrets/copySecret.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { EnvironmentSecret, OrgSecret, RepoSecret } from "../../model"; 4 | 5 | interface CopySecretCommandArgs { 6 | gitHubRepoContext: GitHubRepoContext; 7 | secret: RepoSecret | OrgSecret | EnvironmentSecret; 8 | } 9 | 10 | export function registerCopySecret(context: vscode.ExtensionContext) { 11 | context.subscriptions.push( 12 | vscode.commands.registerCommand( 13 | "github-actions.settings.secret.copy", 14 | async (args: CopySecretCommandArgs) => { 15 | const { secret } = args; 16 | 17 | vscode.env.clipboard.writeText(secret.name); 18 | 19 | vscode.window.setStatusBarMessage(`Copied ${secret.name}`, 2000); 20 | } 21 | ) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/secrets/deleteSecret.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { RepoSecret } from "../../model"; 4 | 5 | interface DeleteSecretCommandArgs { 6 | gitHubRepoContext: GitHubRepoContext; 7 | secret: RepoSecret; 8 | } 9 | 10 | export function registerDeleteSecret(context: vscode.ExtensionContext) { 11 | context.subscriptions.push( 12 | vscode.commands.registerCommand( 13 | "github-actions.settings.secret.delete", 14 | async (args: DeleteSecretCommandArgs) => { 15 | const gitHubContext = args.gitHubRepoContext; 16 | const secret = args.secret; 17 | 18 | await gitHubContext.client.actions.deleteRepoSecret({ 19 | owner: gitHubContext.owner, 20 | repo: gitHubContext.name, 21 | secret_name: secret.name, 22 | }); 23 | 24 | vscode.commands.executeCommand("github-actions.explorer.refresh"); 25 | } 26 | ) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/secrets/manageOrgSecrets.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { WorkflowRun } from "../../model"; 4 | 5 | interface ManageOrgSecretsCommandArgs { 6 | gitHubRepoContext: GitHubRepoContext; 7 | run: WorkflowRun; 8 | } 9 | 10 | export function registerManageOrgSecrets(context: vscode.ExtensionContext) { 11 | context.subscriptions.push( 12 | vscode.commands.registerCommand( 13 | "github-actions.settings.secrets.manage", 14 | async (args: ManageOrgSecretsCommandArgs) => { 15 | const gitHubContext = args.gitHubRepoContext; 16 | 17 | // Open link to manage org-secrets 18 | vscode.commands.executeCommand( 19 | "vscode.open", 20 | vscode.Uri.parse( 21 | `https://github.com/organizations/${gitHubContext.owner}/settings/secrets` 22 | ) 23 | ); 24 | } 25 | ) 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/secrets/updateSecret.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { RepoSecret } from "../../model"; 4 | import { encodeSecret } from "../../secrets"; 5 | 6 | interface UpdateSecretCommandArgs { 7 | gitHubRepoContext: GitHubRepoContext; 8 | secret: RepoSecret; 9 | } 10 | 11 | export function registerUpdateSecret(context: vscode.ExtensionContext) { 12 | context.subscriptions.push( 13 | vscode.commands.registerCommand( 14 | "github-actions.settings.secret.update", 15 | async (args: UpdateSecretCommandArgs) => { 16 | const gitHubContext = args.gitHubRepoContext; 17 | const secret: RepoSecret = args.secret; 18 | 19 | const value = await vscode.window.showInputBox({ 20 | prompt: "Enter the new secret value", 21 | }); 22 | 23 | if (!value) { 24 | return; 25 | } 26 | 27 | try { 28 | const keyResponse = 29 | await gitHubContext.client.actions.getRepoPublicKey({ 30 | owner: gitHubContext.owner, 31 | repo: gitHubContext.name, 32 | }); 33 | 34 | const key_id = keyResponse.data.key_id; 35 | const key = keyResponse.data.key; 36 | 37 | await gitHubContext.client.actions.createOrUpdateRepoSecret({ 38 | owner: gitHubContext.owner, 39 | repo: gitHubContext.name, 40 | secret_name: secret.name, 41 | key_id: key_id, 42 | encrypted_value: encodeSecret(key, value), 43 | }); 44 | } catch (e) { 45 | vscode.window.showErrorMessage(e.message); 46 | } 47 | } 48 | ) 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/commands/triggerWorkflowRun.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { 4 | GitHubRepoContext, 5 | getGitHead, 6 | getGitHubContextForWorkspaceUri, 7 | } from "../git/repository"; 8 | import { getWorkflowUri, parseWorkflow } from "../workflow/workflow"; 9 | 10 | import { Workflow } from "../model"; 11 | 12 | interface TriggerRunCommandOptions { 13 | wf?: Workflow; 14 | gitHubRepoContext: GitHubRepoContext; 15 | } 16 | 17 | export function registerTriggerWorkflowRun(context: vscode.ExtensionContext) { 18 | context.subscriptions.push( 19 | vscode.commands.registerCommand( 20 | "github-actions.explorer.triggerRun", 21 | async (args: TriggerRunCommandOptions | vscode.Uri) => { 22 | let workflowUri: vscode.Uri | null = null; 23 | if (args instanceof vscode.Uri) { 24 | workflowUri = args; 25 | } else if (args.wf) { 26 | const wf: Workflow = args.wf; 27 | workflowUri = getWorkflowUri(args.gitHubRepoContext, wf.path); 28 | } 29 | 30 | if (!workflowUri) { 31 | return; 32 | } 33 | 34 | // Parse 35 | const workspaceFolder = 36 | vscode.workspace.getWorkspaceFolder(workflowUri); 37 | if (!workspaceFolder) { 38 | return; 39 | } 40 | 41 | const gitHubRepoContext = await getGitHubContextForWorkspaceUri( 42 | workspaceFolder.uri 43 | ); 44 | if (!gitHubRepoContext) { 45 | return; 46 | } 47 | 48 | const workflow = await parseWorkflow(workflowUri, gitHubRepoContext); 49 | if (!workflow) { 50 | return; 51 | } 52 | 53 | let selectedEvent: string | undefined; 54 | if ( 55 | workflow.on.workflow_dispatch !== undefined && 56 | workflow.on.repository_dispatch !== undefined 57 | ) { 58 | selectedEvent = await vscode.window.showQuickPick( 59 | ["repository_dispatch", "workflow_dispatch"], 60 | { 61 | placeHolder: "Which event to trigger?", 62 | } 63 | ); 64 | if (!selectedEvent) { 65 | return; 66 | } 67 | } 68 | 69 | if ( 70 | (!selectedEvent || selectedEvent === "workflow_dispatch") && 71 | workflow.on.workflow_dispatch !== undefined 72 | ) { 73 | const ref = await vscode.window.showInputBox({ 74 | prompt: "Enter ref to trigger workflow on", 75 | value: (await getGitHead()) || gitHubRepoContext.defaultBranch, 76 | }); 77 | 78 | if (ref) { 79 | // Inputs 80 | let inputs: { [key: string]: string } | undefined; 81 | const definedInputs = workflow.on.workflow_dispatch?.inputs; 82 | if (definedInputs) { 83 | inputs = {}; 84 | 85 | for (const definedInput of Object.keys(definedInputs)) { 86 | const value = await vscode.window.showInputBox({ 87 | prompt: `Value for input ${definedInput} ${ 88 | definedInputs[definedInput].required ? "[required]" : "" 89 | }`, 90 | value: definedInputs[definedInput].default, 91 | }); 92 | if (!value && definedInputs[definedInput].required) { 93 | vscode.window.showErrorMessage( 94 | `Input ${definedInput} is required` 95 | ); 96 | return; 97 | } 98 | 99 | if (value) { 100 | inputs[definedInput] = value; 101 | } 102 | } 103 | } 104 | 105 | try { 106 | const relativeWorkflowPath = vscode.workspace.asRelativePath( 107 | workflowUri, 108 | false 109 | ); 110 | 111 | await gitHubRepoContext.client.actions.createWorkflowDispatch({ 112 | owner: gitHubRepoContext.owner, 113 | repo: gitHubRepoContext.name, 114 | workflow_id: relativeWorkflowPath, 115 | ref, 116 | inputs, 117 | }); 118 | 119 | vscode.window.setStatusBarMessage( 120 | `GitHub Actions: Workflow event dispatched`, 121 | 2000 122 | ); 123 | } catch (error) { 124 | vscode.window.showErrorMessage( 125 | `Could not create workflow dispatch: ${error.message}` 126 | ); 127 | } 128 | } 129 | } else if ( 130 | (!selectedEvent || selectedEvent === "repository_dispatch") && 131 | workflow.on.repository_dispatch !== undefined 132 | ) { 133 | let event_type: string | undefined; 134 | const event_types = workflow.on.repository_dispatch.types; 135 | if (Array.isArray(event_types) && event_types?.length > 0) { 136 | const custom_type = "✐ Enter custom type"; 137 | const selection = await vscode.window.showQuickPick( 138 | [custom_type, ...event_types], 139 | { 140 | placeHolder: "Select an event_type to dispatch", 141 | } 142 | ); 143 | 144 | if (selection === undefined) { 145 | return; 146 | } else if (selection != custom_type) { 147 | event_type = selection; 148 | } 149 | } 150 | 151 | if (event_type === undefined) { 152 | event_type = await vscode.window.showInputBox({ 153 | prompt: "Enter `event_type` to dispatch to the repository", 154 | value: "default", 155 | }); 156 | } 157 | 158 | if (event_type) { 159 | await gitHubRepoContext.client.repos.createDispatchEvent({ 160 | owner: gitHubRepoContext.owner, 161 | repo: gitHubRepoContext.name, 162 | event_type, 163 | client_payload: {}, 164 | }); 165 | 166 | vscode.window.setStatusBarMessage( 167 | `GitHub Actions: Repository event '${event_type}' dispatched`, 168 | 2000 169 | ); 170 | } 171 | } 172 | 173 | vscode.commands.executeCommand("github-actions.explorer.refresh"); 174 | } 175 | ) 176 | ); 177 | } 178 | -------------------------------------------------------------------------------- /src/commands/unpinWorkflow.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { GitHubRepoContext } from "../git/repository"; 4 | import { Workflow } from "../model"; 5 | import { getWorkflowUri } from "../workflow/workflow"; 6 | import { unpinWorkflow } from "../configuration/configuration"; 7 | 8 | interface UnPinWorkflowCommandOptions { 9 | gitHubRepoContext: GitHubRepoContext; 10 | wf?: Workflow; 11 | 12 | updateContextValue(): void; 13 | } 14 | 15 | export function registerUnPinWorkflow(context: vscode.ExtensionContext) { 16 | context.subscriptions.push( 17 | vscode.commands.registerCommand( 18 | "github-actions.workflow.unpin", 19 | async (args: UnPinWorkflowCommandOptions) => { 20 | const { gitHubRepoContext, wf } = args; 21 | 22 | if (!wf) { 23 | return; 24 | } 25 | 26 | const workflowFullPath = getWorkflowUri(gitHubRepoContext, wf.path); 27 | if (!workflowFullPath) { 28 | return; 29 | } 30 | 31 | const relativeWorkflowPath = 32 | vscode.workspace.asRelativePath(workflowFullPath); 33 | await unpinWorkflow(relativeWorkflowPath); 34 | 35 | args.updateContextValue(); 36 | vscode.commands.executeCommand("github-actions.explorer.refresh"); 37 | } 38 | ) 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/configuration/configuration.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | const settingsKey = "github-actions"; 4 | 5 | export function initConfiguration(context: vscode.ExtensionContext) { 6 | context.subscriptions.push( 7 | vscode.workspace.onDidChangeConfiguration((e) => { 8 | if (e.affectsConfiguration(getSettingsKey("workflows.pinned"))) { 9 | pinnedWorkflowsChangeHandlers.forEach((h) => h()); 10 | } 11 | }) 12 | ); 13 | } 14 | 15 | function getConfiguration() { 16 | return vscode.workspace.getConfiguration(); 17 | } 18 | 19 | function getSettingsKey(settingsPath: string): string { 20 | return `${settingsKey}.${settingsPath}`; 21 | } 22 | 23 | const pinnedWorkflowsChangeHandlers: (() => void)[] = []; 24 | export function onPinnedWorkflowsChange(handler: () => void) { 25 | pinnedWorkflowsChangeHandlers.push(handler); 26 | } 27 | 28 | export function getPinnedWorkflows(): string[] { 29 | return getConfiguration().get( 30 | getSettingsKey("workflows.pinned.workflows"), 31 | [] 32 | ); 33 | } 34 | 35 | export async function pinWorkflow(workflow: string) { 36 | const pinedWorkflows = Array.from( 37 | new Set(getPinnedWorkflows()).add(workflow) 38 | ); 39 | await getConfiguration().update( 40 | getSettingsKey("workflows.pinned.workflows"), 41 | pinedWorkflows 42 | ); 43 | } 44 | 45 | export async function unpinWorkflow(workflow: string) { 46 | const x = new Set(getPinnedWorkflows()); 47 | x.delete(workflow); 48 | const pinnedWorkflows = Array.from(x); 49 | await getConfiguration().update( 50 | getSettingsKey("workflows.pinned.workflows"), 51 | pinnedWorkflows 52 | ); 53 | } 54 | 55 | export function isPinnedWorkflowsRefreshEnabled(): boolean { 56 | return getConfiguration().get( 57 | getSettingsKey("workflows.pinned.refresh.enabled"), 58 | false 59 | ); 60 | } 61 | 62 | export function pinnedWorkflowsRefreshInterval(): number { 63 | return getConfiguration().get( 64 | getSettingsKey("workflows.pinned.refresh.interval"), 65 | 60 66 | ); 67 | } 68 | 69 | export function orgFeaturesEnabled(): boolean { 70 | return getConfiguration().get(getSettingsKey("org-features"), false); 71 | } 72 | 73 | export async function updateOrgFeaturesEnabled(enabled: boolean) { 74 | await getConfiguration().update( 75 | getSettingsKey("org-features"), 76 | enabled, 77 | true 78 | ); 79 | } 80 | 81 | export function getRemoteName(): string { 82 | return getConfiguration().get( 83 | getSettingsKey("remote-name"), 84 | "origin" 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { init as initLogger, log, logDebug } from "./log"; 4 | 5 | import { registerCancelWorkflowRun } from "./commands/cancelWorkflowRun"; 6 | import { registerOpenWorkflowFile } from "./commands/openWorkflowFile"; 7 | import { registerOpenWorkflowRun } from "./commands/openWorkflowRun"; 8 | import { registerOpenWorkflowRunLogs } from "./commands/openWorkflowRunLogs"; 9 | import { registerOrgLogin } from "./commands/orgLogin"; 10 | import { registerPinWorkflow } from "./commands/pinWorkflow"; 11 | import { registerReRunWorkflowRun } from "./commands/rerunWorkflowRun"; 12 | import { registerAddSecret } from "./commands/secrets/addSecret"; 13 | import { registerCopySecret } from "./commands/secrets/copySecret"; 14 | import { registerDeleteSecret } from "./commands/secrets/deleteSecret"; 15 | import { registerManageOrgSecrets } from "./commands/secrets/manageOrgSecrets"; 16 | import { registerUpdateSecret } from "./commands/secrets/updateSecret"; 17 | import { registerTriggerWorkflowRun } from "./commands/triggerWorkflowRun"; 18 | import { registerUnPinWorkflow } from "./commands/unpinWorkflow"; 19 | import { initConfiguration } from "./configuration/configuration"; 20 | import { getGitHubContext } from "./git/repository"; 21 | import { LogScheme } from "./logs/constants"; 22 | import { WorkflowStepLogProvider } from "./logs/fileProvider"; 23 | import { WorkflowStepLogFoldingProvider } from "./logs/foldingProvider"; 24 | import { WorkflowStepLogSymbolProvider } from "./logs/symbolProvider"; 25 | import { initPinnedWorkflows } from "./pinnedWorkflows/pinnedWorkflows"; 26 | import { initWorkflowDocumentTracking } from "./tracker/workflowDocumentTracker"; 27 | import { CurrentBranchTreeProvider } from "./treeViews/currentBranch"; 28 | import { initResources } from "./treeViews/icons"; 29 | import { SettingsTreeProvider } from "./treeViews/settings"; 30 | import { WorkflowsTreeProvider } from "./treeViews/workflows"; 31 | import { init } from "./workflow/diagnostics"; 32 | 33 | export function activate(context: vscode.ExtensionContext) { 34 | initLogger(); 35 | log("Activating GitHub Actions extension..."); 36 | 37 | // Prefetch git repository origin url 38 | getGitHubContext(); 39 | 40 | initResources(context); 41 | 42 | initConfiguration(context); 43 | initPinnedWorkflows(context); 44 | 45 | // Track workflow 46 | initWorkflowDocumentTracking(context); 47 | 48 | // 49 | // Tree views 50 | // 51 | 52 | const workflowTreeProvider = new WorkflowsTreeProvider(); 53 | context.subscriptions.push( 54 | vscode.window.registerTreeDataProvider( 55 | "github-actions.workflows", 56 | workflowTreeProvider 57 | ) 58 | ); 59 | 60 | const settingsTreeProvider = new SettingsTreeProvider(); 61 | context.subscriptions.push( 62 | vscode.window.registerTreeDataProvider( 63 | "github-actions.settings", 64 | settingsTreeProvider 65 | ) 66 | ); 67 | 68 | const currentBranchTreeProvider = new CurrentBranchTreeProvider(); 69 | context.subscriptions.push( 70 | vscode.window.registerTreeDataProvider( 71 | "github-actions.current-branch", 72 | currentBranchTreeProvider 73 | ) 74 | ); 75 | 76 | context.subscriptions.push( 77 | vscode.commands.registerCommand("github-actions.explorer.refresh", () => { 78 | workflowTreeProvider.refresh(); 79 | settingsTreeProvider.refresh(); 80 | }) 81 | ); 82 | 83 | context.subscriptions.push( 84 | vscode.commands.registerCommand( 85 | "github-actions.explorer.current-branch.refresh", 86 | () => { 87 | currentBranchTreeProvider.refresh(); 88 | } 89 | ) 90 | ); 91 | 92 | (async () => { 93 | const context = await getGitHubContext(); 94 | if (!context) { 95 | logDebug("Could not register branch change event handler"); 96 | return; 97 | } 98 | 99 | for (const repo of context.repos) { 100 | if (!repo.repositoryState) { 101 | continue; 102 | } 103 | 104 | let currentAhead = repo.repositoryState.HEAD?.ahead; 105 | let currentHeadName = repo.repositoryState.HEAD?.name; 106 | repo.repositoryState.onDidChange(() => { 107 | // When the current head/branch changes, or the number of commits ahead changes (which indicates 108 | // a push), refresh the current-branch view 109 | if ( 110 | repo.repositoryState!.HEAD?.name !== currentHeadName || 111 | (repo.repositoryState!.HEAD?.ahead || 0) < (currentAhead || 0) 112 | ) { 113 | currentHeadName = repo.repositoryState!.HEAD?.name; 114 | currentAhead = repo.repositoryState!.HEAD?.ahead; 115 | currentBranchTreeProvider.refresh(); 116 | } 117 | }); 118 | } 119 | })(); 120 | 121 | // 122 | // Commands 123 | // 124 | 125 | registerOpenWorkflowRun(context); 126 | registerOpenWorkflowFile(context); 127 | registerOpenWorkflowRunLogs(context); 128 | registerTriggerWorkflowRun(context); 129 | registerReRunWorkflowRun(context); 130 | registerCancelWorkflowRun(context); 131 | 132 | registerManageOrgSecrets(context); 133 | registerAddSecret(context); 134 | registerDeleteSecret(context); 135 | registerCopySecret(context); 136 | registerUpdateSecret(context); 137 | 138 | registerOrgLogin(context); 139 | 140 | registerPinWorkflow(context); 141 | registerUnPinWorkflow(context); 142 | 143 | // 144 | // Log providers 145 | // 146 | context.subscriptions.push( 147 | vscode.workspace.registerTextDocumentContentProvider( 148 | LogScheme, 149 | new WorkflowStepLogProvider() 150 | ) 151 | ); 152 | 153 | context.subscriptions.push( 154 | vscode.languages.registerFoldingRangeProvider( 155 | { scheme: LogScheme }, 156 | new WorkflowStepLogFoldingProvider() 157 | ) 158 | ); 159 | 160 | context.subscriptions.push( 161 | vscode.languages.registerDocumentSymbolProvider( 162 | { 163 | scheme: LogScheme, 164 | }, 165 | new WorkflowStepLogSymbolProvider() 166 | ) 167 | ); 168 | 169 | // 170 | // Editing features 171 | // 172 | init(context); 173 | 174 | log("...initialized"); 175 | } 176 | -------------------------------------------------------------------------------- /src/external/README.md: -------------------------------------------------------------------------------- 1 | Code in here is forked from the Microsoft GitHub Pull Request extension 2 | https://github.com/microsoft/vscode-pull-request-github/ -------------------------------------------------------------------------------- /src/external/protocol.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from "vscode"; 7 | 8 | import { resolve } from "./ssh"; 9 | 10 | export enum ProtocolType { 11 | Local, 12 | HTTP, 13 | SSH, 14 | GIT, 15 | OTHER 16 | } 17 | 18 | export class Protocol { 19 | public type: ProtocolType = ProtocolType.OTHER; 20 | public host: string = ""; 21 | 22 | public owner: string = ""; 23 | 24 | public repositoryName: string = ""; 25 | 26 | public get nameWithOwner(): string { 27 | return this.owner 28 | ? `${this.owner}/${this.repositoryName}` 29 | : this.repositoryName; 30 | } 31 | 32 | //@ts-ignore 33 | public readonly url: vscode.Uri; 34 | 35 | constructor(uriString: string) { 36 | if (this.parseSshProtocol(uriString)) { 37 | return; 38 | } 39 | 40 | try { 41 | this.url = vscode.Uri.parse(uriString); 42 | this.type = this.getType(this.url.scheme); 43 | 44 | this.host = this.getHostName(this.url.authority); 45 | if (this.host) { 46 | this.repositoryName = this.getRepositoryName(this.url.path) || ""; 47 | this.owner = this.getOwnerName(this.url.path) || ""; 48 | } 49 | } catch (e) { 50 | // Logger.appendLine(`Failed to parse '${uriString}'`); 51 | vscode.window.showWarningMessage( 52 | `Unable to parse remote '${uriString}'. Please check that it is correctly formatted.` 53 | ); 54 | } 55 | } 56 | 57 | private getType(scheme: string): ProtocolType { 58 | switch (scheme) { 59 | case "file": 60 | return ProtocolType.Local; 61 | case "http": 62 | case "https": 63 | return ProtocolType.HTTP; 64 | case "git": 65 | return ProtocolType.GIT; 66 | case "ssh": 67 | return ProtocolType.SSH; 68 | default: 69 | return ProtocolType.OTHER; 70 | } 71 | } 72 | 73 | private parseSshProtocol(uriString: string): boolean { 74 | const sshConfig = resolve(uriString); 75 | if (!sshConfig) { 76 | return false; 77 | } 78 | const { HostName, path } = sshConfig; 79 | this.host = HostName; 80 | this.owner = this.getOwnerName(path) || ""; 81 | this.repositoryName = this.getRepositoryName(path) || ""; 82 | this.type = ProtocolType.SSH; 83 | return true; 84 | } 85 | 86 | getHostName(authority: string) { 87 | // :@: 88 | const matches = /^(?:.*:?@)?([^:]*)(?::.*)?$/.exec(authority); 89 | 90 | if (matches && matches.length >= 2) { 91 | // normalize to fix #903. 92 | // www.github.com will redirect anyways, so this is safe in this specific case, but potentially not in others. 93 | return matches[1].toLocaleLowerCase() === "www.github.com" 94 | ? "github.com" 95 | : matches[1]; 96 | } 97 | 98 | return ""; 99 | } 100 | 101 | getRepositoryName(path: string) { 102 | let normalized = path.replace(/\\/g, "/"); 103 | if (normalized.endsWith("/")) { 104 | normalized = normalized.substr(0, normalized.length - 1); 105 | } 106 | const lastIndex = normalized.lastIndexOf("/"); 107 | const lastSegment = normalized.substr(lastIndex + 1); 108 | if (lastSegment === "" || lastSegment === "/") { 109 | return; 110 | } 111 | 112 | return lastSegment.replace(/\/$/, "").replace(/\.git$/, ""); 113 | } 114 | 115 | getOwnerName(path: string) { 116 | let normalized = path.replace(/\\/g, "/"); 117 | if (normalized.endsWith("/")) { 118 | normalized = normalized.substr(0, normalized.length - 1); 119 | } 120 | 121 | const fragments = normalized.split("/"); 122 | if (fragments.length > 1) { 123 | return fragments[fragments.length - 2]; 124 | } 125 | 126 | return; 127 | } 128 | 129 | normalizeUri(): vscode.Uri | undefined { 130 | if (this.type === ProtocolType.OTHER && !this.url) { 131 | return; 132 | } 133 | 134 | if (this.type === ProtocolType.Local) { 135 | return this.url; 136 | } 137 | 138 | let scheme = "https"; 139 | if ( 140 | this.url && 141 | (this.url.scheme === "http" || this.url.scheme === "https") 142 | ) { 143 | scheme = this.url.scheme; 144 | } 145 | 146 | try { 147 | return vscode.Uri.parse( 148 | `${scheme}://${this.host.toLocaleLowerCase()}/${this.nameWithOwner.toLocaleLowerCase()}` 149 | ); 150 | } catch (e) { 151 | return; 152 | } 153 | } 154 | 155 | toString(): string | undefined { 156 | // based on Uri scheme for SSH https://tools.ietf.org/id/draft-salowey-secsh-uri-00.html#anchor1 and heuristics of how GitHub handles ssh url 157 | // sshUri = `ssh:` 158 | // - omitted 159 | // hier-part = "//" authority path-abempty 160 | // - // is omitted 161 | // authority = [ [ ssh-info ] "@" host ] [ ":" port] 162 | // - ssh-info: git 163 | // - host: ${this.host} 164 | // - port: omitted 165 | // path-abempty = 166 | // - we use relative path here `${this.owner}/${this.repositoryName}` 167 | if (this.type === ProtocolType.SSH) { 168 | return `git@${this.host}:${this.owner}/${this.repositoryName}`; 169 | } 170 | 171 | if (this.type === ProtocolType.GIT) { 172 | return `git://git@${this.host}:${this.owner}/${this.repositoryName}`; 173 | } 174 | 175 | const normalizedUri = this.normalizeUri(); 176 | if (normalizedUri) { 177 | return normalizedUri.toString(); 178 | } 179 | 180 | return; 181 | } 182 | 183 | update(change: { 184 | type?: ProtocolType; 185 | host?: string; 186 | owner?: string; 187 | repositoryName?: string; 188 | }): Protocol { 189 | if (change.type) { 190 | this.type = change.type; 191 | } 192 | 193 | if (change.host) { 194 | this.host = change.host; 195 | } 196 | 197 | if (change.owner) { 198 | this.owner = change.owner; 199 | } 200 | 201 | if (change.repositoryName) { 202 | this.repositoryName = change.repositoryName; 203 | } 204 | 205 | return this; 206 | } 207 | 208 | equals(other: Protocol) { 209 | const normalizeUri = this.normalizeUri(); 210 | if (!normalizeUri) { 211 | return false; 212 | } 213 | 214 | const otherNormalizeUri = other.normalizeUri(); 215 | if (!otherNormalizeUri) { 216 | return false; 217 | } 218 | 219 | return ( 220 | normalizeUri.toString().toLocaleLowerCase() === 221 | otherNormalizeUri.toString().toLocaleLowerCase() 222 | ); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/external/ssh.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseConfig } from "ssh-config"; 2 | 3 | const SSH_URL_RE = /^(?:([^@:]+)@)?([^:/]+):?(.+)$/; 4 | const URL_SCHEME_RE = /^([a-z-]+):\/\//; 5 | 6 | /** 7 | * SSH Config interface 8 | * 9 | * Note that this interface atypically capitalizes field names. This is for consistency 10 | * with SSH config files. 11 | */ 12 | export interface Config { 13 | Host: string; 14 | [param: string]: string; 15 | } 16 | 17 | /** 18 | * ConfigResolvers take a config, resolve some additional data (perhaps using 19 | * a config file), and return a new Config. 20 | */ 21 | export type ConfigResolver = (config: Config) => Config; 22 | 23 | /** 24 | * Parse and resolve an SSH url. Resolves host aliases using the configuration 25 | * specified by ~/.ssh/config, if present. 26 | * 27 | * Examples: 28 | * 29 | * resolve("git@github.com:Microsoft/vscode") 30 | * { 31 | * Host: 'github.com', 32 | * HostName: 'github.com', 33 | * User: 'git', 34 | * path: 'Microsoft/vscode', 35 | * } 36 | * 37 | * resolve("hub:queerviolet/vscode", resolverFromConfig("Host hub\n HostName github.com\n User git\n")) 38 | * { 39 | * Host: 'hub', 40 | * HostName: 'github.com', 41 | * User: 'git', 42 | * path: 'queerviolet/vscode', 43 | * } 44 | * 45 | * @param {string} url the url to parse 46 | * @param {ConfigResolver?} resolveConfig ssh config resolver (default: from ~/.ssh/config) 47 | * @returns {Config} 48 | */ 49 | export const resolve = (url: string, resolveConfig = Resolvers.current) => { 50 | const config = parse(url); 51 | return config && resolveConfig(config); 52 | }; 53 | 54 | export class Resolvers { 55 | static default = chainResolvers(baseResolver /*, resolverFromConfigFile()*/); 56 | 57 | static fromConfig(conf: string) { 58 | return chainResolvers(baseResolver, resolverFromConfig(conf)); 59 | } 60 | 61 | static current = Resolvers.default; 62 | } 63 | 64 | const parse = (url: string): Config | undefined => { 65 | const urlMatch = URL_SCHEME_RE.exec(url); 66 | if (urlMatch) { 67 | const [fullSchemePrefix, scheme] = urlMatch; 68 | if (scheme === "ssh") { 69 | url = url.slice(fullSchemePrefix.length); 70 | } else { 71 | return; 72 | } 73 | } 74 | const match = SSH_URL_RE.exec(url); 75 | if (!match) { 76 | return; 77 | } 78 | const [, User, Host, path] = match; 79 | return { User, Host, path }; 80 | }; 81 | 82 | function baseResolver(config: Config) { 83 | return { 84 | ...config, 85 | HostName: config.Host 86 | }; 87 | } 88 | 89 | // Temporarily disable this to remove `fs` dependency 90 | // function resolverFromConfigFile( 91 | // configPath = join(homedir(), ".ssh", "config") 92 | // ): ConfigResolver | undefined { 93 | // try { 94 | // const config = readFileSync(configPath).toString(); 95 | // return resolverFromConfig(config); 96 | // } catch (error) { 97 | // // Logger.appendLine(`${configPath}: ${error.message}`); 98 | // } 99 | // } 100 | 101 | export function resolverFromConfig(text: string): ConfigResolver { 102 | const config = parseConfig(text); 103 | return h => config.compute(h.Host); 104 | } 105 | 106 | function chainResolvers( 107 | ...chain: (ConfigResolver | undefined)[] 108 | ): ConfigResolver { 109 | const resolvers = chain.filter(x => !!x) as ConfigResolver[]; 110 | return (config: Config) => 111 | resolvers.reduce( 112 | (resolved, next) => ({ 113 | ...resolved, 114 | ...next(resolved) 115 | }), 116 | config 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /src/git/repository.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { logDebug, logError } from "../log"; 4 | import { API, GitExtension, RefType, RepositoryState } from "../typings/git"; 5 | 6 | import { Octokit } from "@octokit/rest"; 7 | import { getClient } from "../api/api"; 8 | import { getSession } from "../auth/auth"; 9 | import { getRemoteName } from "../configuration/configuration"; 10 | import { Protocol } from "../external/protocol"; 11 | 12 | async function getGitExtension(): Promise { 13 | const gitExtension = 14 | vscode.extensions.getExtension("vscode.git"); 15 | if (gitExtension) { 16 | if (!gitExtension.isActive) { 17 | await gitExtension.activate(); 18 | } 19 | const git = gitExtension.exports.getAPI(1); 20 | 21 | if (git.state !== "initialized") { 22 | // Wait for the plugin to be initialized 23 | await new Promise((resolve) => { 24 | if (git.state === "initialized") { 25 | resolve(); 26 | } else { 27 | const listener = git.onDidChangeState((state) => { 28 | if (state === "initialized") { 29 | resolve(); 30 | } 31 | listener.dispose(); 32 | }); 33 | } 34 | }); 35 | } 36 | 37 | return git; 38 | } 39 | } 40 | 41 | export async function getGitHead(): Promise { 42 | const git = await getGitExtension(); 43 | if (git && git.repositories.length > 0) { 44 | const head = git.repositories[0].state.HEAD; 45 | if (head && head.name && head.type === RefType.Head) { 46 | return `refs/heads/${head.name}`; 47 | } 48 | } 49 | } 50 | export async function getGitHubUrls(): Promise< 51 | | { 52 | workspaceUri: vscode.Uri; 53 | url: string; 54 | protocol: Protocol; 55 | }[] 56 | | null 57 | > { 58 | const git = await getGitExtension(); 59 | if (git && git.repositories.length > 0) { 60 | logDebug("Found git extension"); 61 | 62 | const remoteName = getRemoteName(); 63 | 64 | const p = await Promise.all( 65 | git.repositories.map(async (r) => { 66 | logDebug("Find `origin` remote for repository", r.rootUri.path); 67 | await r.status(); 68 | 69 | const originRemote = r.state.remotes.filter( 70 | (remote) => remote.name === remoteName 71 | ); 72 | if ( 73 | originRemote.length > 0 && 74 | originRemote[0].pushUrl?.indexOf("github.com") !== -1 75 | ) { 76 | const url = originRemote[0].pushUrl!; 77 | 78 | return { 79 | workspaceUri: r.rootUri, 80 | url, 81 | protocol: new Protocol(url), 82 | }; 83 | } 84 | 85 | logDebug(`Remote "${remoteName}" not found, skipping repository`); 86 | 87 | return undefined; 88 | }) 89 | ); 90 | return p.filter((x) => !!x) as any; 91 | } 92 | 93 | // If we cannot find the git extension, assume for now that we are running a web context, 94 | // for instance, github.dev. I think ideally we'd check the workspace URIs first, but this 95 | // works for now. We'll revisit later. 96 | // if (!git) { 97 | // Support for virtual workspaces 98 | const isVirtualWorkspace = 99 | vscode.workspace.workspaceFolders && 100 | vscode.workspace.workspaceFolders.every((f) => f.uri.scheme !== "file"); 101 | if (isVirtualWorkspace) { 102 | logDebug("Found virtual workspace"); 103 | 104 | const ghFolder = vscode.workspace.workspaceFolders?.find( 105 | (x) => x.uri.scheme === "vscode-vfs" && x.uri.authority === "github" 106 | ); 107 | if (ghFolder) { 108 | logDebug("Found virtual GitHub workspace folder"); 109 | 110 | const url = `https://github.com/${ghFolder.uri.path}`; 111 | 112 | return [ 113 | { 114 | workspaceUri: ghFolder.uri, 115 | url: url, 116 | protocol: new Protocol(url), 117 | }, 118 | ]; 119 | } 120 | // } 121 | } 122 | 123 | return null; 124 | } 125 | 126 | export interface GitHubRepoContext { 127 | client: Octokit; 128 | repositoryState: RepositoryState | undefined; 129 | 130 | workspaceUri: vscode.Uri; 131 | 132 | id: number; 133 | owner: string; 134 | name: string; 135 | 136 | defaultBranch: string; 137 | 138 | ownerIsOrg: boolean; 139 | orgFeaturesEnabled?: boolean; 140 | } 141 | 142 | export interface GitHubContext { 143 | repos: GitHubRepoContext[]; 144 | reposByUri: Map; 145 | } 146 | 147 | let gitHubContext: Promise | undefined; 148 | 149 | export async function getGitHubContext(): Promise { 150 | if (gitHubContext) { 151 | return gitHubContext; 152 | } 153 | 154 | try { 155 | const git = await getGitExtension(); 156 | // if (!git) { 157 | // logDebug("Could not find git extension"); 158 | // return; 159 | // } 160 | 161 | const session = await getSession(); 162 | const client = getClient(session.accessToken); 163 | 164 | const protocolInfos = await getGitHubUrls(); 165 | if (!protocolInfos) { 166 | logDebug("Could not get protocol infos"); 167 | return undefined; 168 | } 169 | 170 | logDebug("Found protocol infos", protocolInfos.length.toString()); 171 | 172 | const repos = await Promise.all( 173 | protocolInfos.map(async (protocolInfo): Promise => { 174 | logDebug("Getting infos for repository", protocolInfo.url); 175 | 176 | const repoInfo = await client.repos.get({ 177 | repo: protocolInfo.protocol.repositoryName, 178 | owner: protocolInfo.protocol.owner, 179 | }); 180 | 181 | const repo = git && git.getRepository(protocolInfo.workspaceUri); 182 | 183 | return { 184 | workspaceUri: protocolInfo.workspaceUri, 185 | client, 186 | repositoryState: repo?.state, 187 | name: protocolInfo.protocol.repositoryName, 188 | owner: protocolInfo.protocol.owner, 189 | id: repoInfo.data.id, 190 | defaultBranch: `refs/heads/${repoInfo.data.default_branch}`, 191 | ownerIsOrg: repoInfo.data.owner?.type === "Organization", 192 | orgFeaturesEnabled: 193 | session.scopes.find((x) => x.toLocaleLowerCase() === "admin:org") != 194 | null, 195 | }; 196 | }) 197 | ); 198 | 199 | gitHubContext = Promise.resolve({ 200 | repos, 201 | reposByUri: new Map(repos.map((r) => [r.workspaceUri.toString(), r])), 202 | }); 203 | } catch (e: any) { 204 | // Reset the context so the next attempt will try this flow again 205 | gitHubContext = undefined; 206 | 207 | logError(e, "Error getting GitHub context"); 208 | 209 | // Rethrow original error 210 | throw e; 211 | } 212 | 213 | return gitHubContext; 214 | } 215 | 216 | export async function resetGitHubContext() { 217 | gitHubContext = undefined; 218 | await getGitHubContext(); 219 | } 220 | 221 | export async function getGitHubContextForRepo( 222 | owner: string, 223 | name: string 224 | ): Promise { 225 | const gitHubContext = await getGitHubContext(); 226 | if (!gitHubContext) { 227 | return undefined; 228 | } 229 | 230 | return gitHubContext.repos.find((r) => r.owner === owner && r.name === name); 231 | } 232 | 233 | export async function getGitHubContextForWorkspaceUri( 234 | workspaceUri: vscode.Uri 235 | ): Promise { 236 | const gitHubContext = await getGitHubContext(); 237 | if (!gitHubContext) { 238 | return undefined; 239 | } 240 | 241 | return gitHubContext.reposByUri.get(workspaceUri.toString()); 242 | } 243 | 244 | export async function getGitHubContextForDocumentUri( 245 | documentUri: vscode.Uri 246 | ): Promise { 247 | const gitHubContext = await getGitHubContext(); 248 | if (!gitHubContext) { 249 | return undefined; 250 | } 251 | 252 | const workspaceUri = vscode.workspace.getWorkspaceFolder(documentUri); 253 | if (!workspaceUri) { 254 | return; 255 | } 256 | 257 | return getGitHubContextForWorkspaceUri(workspaceUri.uri); 258 | } 259 | 260 | export function getCurrentBranch( 261 | state: RepositoryState | undefined 262 | ): string | undefined { 263 | if (!state) { 264 | return; 265 | } 266 | 267 | const head = state.HEAD; 268 | if (!head) { 269 | return; 270 | } 271 | 272 | if (head.type != RefType.Head) { 273 | return; 274 | } 275 | 276 | return head.name; 277 | } 278 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | enum LogLevel { 4 | Debug, 5 | Info, 6 | } 7 | 8 | var logger: vscode.OutputChannel; 9 | var level: LogLevel = LogLevel.Debug; 10 | 11 | export function init() { 12 | logger = vscode.window.createOutputChannel("GitHub Actions"); 13 | } 14 | 15 | export function log(...values: string[]) { 16 | logger.appendLine(values.join(" ")); 17 | } 18 | 19 | export function logDebug(...values: string[]) { 20 | if (level > LogLevel.Debug) { 21 | return; 22 | } 23 | 24 | logger.appendLine(values.join(" ")); 25 | } 26 | 27 | export function logError(e: Error, ...values: string[]) { 28 | logger.appendLine(values.join(" ")); 29 | logger.appendLine(e.message); 30 | if (e.stack) { 31 | logger.appendLine(e.stack); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/logs/ansi.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 2020-02-01T21:15:11.0908855Z npm ci 3 | 2020-02-01T21:15:11.0909011Z npm run build --if-present 4 | 2020-02-01T21:15:11.0909146Z npm test 5 | */ 6 | -------------------------------------------------------------------------------- /src/logs/constants.ts: -------------------------------------------------------------------------------- 1 | export const LogScheme = "gh-actions"; 2 | -------------------------------------------------------------------------------- /src/logs/fileProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { getGitHubContextForRepo } from "../git/repository"; 3 | import { cacheLogInfo } from "./logInfoProvider"; 4 | import { parseLog } from "./model"; 5 | import { parseUri } from "./scheme"; 6 | 7 | export class WorkflowStepLogProvider 8 | implements vscode.TextDocumentContentProvider 9 | { 10 | onDidChangeEmitter = new vscode.EventEmitter(); 11 | onDidChange = this.onDidChangeEmitter.event; 12 | 13 | async provideTextDocumentContent(uri: vscode.Uri): Promise { 14 | const { owner, repo, jobId } = parseUri(uri); 15 | 16 | const githubRepoContext = await getGitHubContextForRepo(owner, repo); 17 | if (!githubRepoContext) { 18 | throw new Error("Could not load logs"); 19 | } 20 | 21 | try { 22 | const result = 23 | await githubRepoContext?.client.actions.downloadJobLogsForWorkflowRun({ 24 | owner: owner, 25 | repo: repo, 26 | job_id: jobId, 27 | }); 28 | 29 | const log = result.data as any; 30 | 31 | const logInfo = parseLog(log); 32 | cacheLogInfo(uri, logInfo); 33 | 34 | return logInfo.updatedLog; 35 | } catch (e) { 36 | if ("status" in e && e.status === 410) { 37 | cacheLogInfo(uri, { 38 | colorFormats: [], 39 | sections: [], 40 | updatedLog: "", 41 | }); 42 | 43 | return "Could not open logs, they are expired."; 44 | } 45 | 46 | console.error("Error loading logs", e); 47 | return `Could not open logs, unhandled error: ${e?.message || e}`; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/logs/foldingProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { getLogInfo } from "./logInfoProvider"; 3 | 4 | export class WorkflowStepLogFoldingProvider 5 | implements vscode.FoldingRangeProvider { 6 | provideFoldingRanges( 7 | document: vscode.TextDocument, 8 | context: vscode.FoldingContext, 9 | token: vscode.CancellationToken 10 | ): vscode.ProviderResult { 11 | const logInfo = getLogInfo(document.uri); 12 | if (!logInfo) { 13 | return []; 14 | } 15 | 16 | return logInfo.sections.map( 17 | s => 18 | new vscode.FoldingRange(s.start, s.end, vscode.FoldingRangeKind.Region) 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/logs/formatProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { LogInfo } from "./model"; 3 | 4 | const timestampRE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{7}Z/; 5 | 6 | const timestampDecorationType = vscode.window.createTextEditorDecorationType({ 7 | color: "#99999959" 8 | }); 9 | 10 | const background = { 11 | "40": "#0c0c0c", 12 | "41": "#e74856", 13 | "42": "#16c60c", 14 | "43": "#f9f1a5", 15 | "44": "#0037da", 16 | "45": "#881798", 17 | "46": "#3a96dd", 18 | "47": "#cccccc", 19 | "100": "#767676" 20 | } as { [key: string]: string }; 21 | 22 | const foreground = { 23 | "30": "#0c0c0c", 24 | "31": "#e74856", 25 | "32": "#16c60c", 26 | "33": "#f9f1a5", 27 | "34": "#0037da", 28 | "35": "#881798", 29 | "36": "#3a96dd", 30 | "37": "#cccccc", 31 | "90": "#767676" 32 | } as { [key: string]: string }; 33 | 34 | export function updateDecorations( 35 | activeEditor: vscode.TextEditor, 36 | logInfo: LogInfo 37 | ) { 38 | if (!activeEditor) { 39 | return; 40 | } 41 | 42 | // Decorate timestamps 43 | const numberOfLines = activeEditor.document.lineCount; 44 | activeEditor.setDecorations( 45 | timestampDecorationType, 46 | Array.from(Array(numberOfLines).keys()) 47 | .filter(i => { 48 | const line = activeEditor.document.lineAt(i).text; 49 | return timestampRE.test(line); 50 | }) 51 | .map(i => ({ 52 | range: new vscode.Range(i, 0, i, 28) // timestamps always have 28 chars 53 | })) 54 | ); 55 | 56 | // Custom colors 57 | const ctypes: { 58 | [key: string]: { type: vscode.TextEditorDecorationType; ranges: any[] }; 59 | } = {}; 60 | 61 | for (const colorFormat of logInfo.colorFormats) { 62 | const range = new vscode.Range( 63 | colorFormat.line, 64 | colorFormat.start, 65 | colorFormat.line, 66 | colorFormat.end 67 | ); 68 | 69 | const key = `${colorFormat.color.foreground}-${colorFormat.color.background}`; 70 | if (!ctypes[key]) { 71 | ctypes[key] = { 72 | type: vscode.window.createTextEditorDecorationType({ 73 | color: 74 | colorFormat.color.foreground && 75 | foreground[colorFormat.color.foreground], 76 | backgroundColor: 77 | colorFormat.color.background && 78 | background[colorFormat.color.background] 79 | }), 80 | ranges: [range] 81 | }; 82 | } else { 83 | ctypes[key].ranges.push(range); 84 | } 85 | } 86 | 87 | for (const ctype of Object.values(ctypes)) { 88 | activeEditor.setDecorations(ctype.type, ctype.ranges); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/logs/logInfoProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { LogInfo } from "./model"; 3 | 4 | const cache = new Map(); 5 | 6 | export function cacheLogInfo(uri: vscode.Uri, logInfo: LogInfo) { 7 | cache.set(uri.toString(), logInfo); 8 | } 9 | 10 | export function getLogInfo(uri: vscode.Uri): LogInfo | undefined { 11 | return cache.get(uri.toString()); 12 | } 13 | -------------------------------------------------------------------------------- /src/logs/model.ts: -------------------------------------------------------------------------------- 1 | const ansiColorRE = /\u001b\[((?:\d+;?)+)m(.*)\u001b\[0m/gm; 2 | const groupMarker = "##[group]"; 3 | const commandRE = /##\[[a-z]+\]/gm; 4 | 5 | export enum Type { 6 | Setup, 7 | Step 8 | } 9 | 10 | export interface LogSection { 11 | type: Type; 12 | start: number; 13 | end: number; 14 | name?: string; 15 | } 16 | 17 | export interface LogColorInfo { 18 | line: number; 19 | start: number; 20 | end: number; 21 | 22 | color: CustomColor; 23 | } 24 | 25 | export interface CustomColor { 26 | foreground?: string; 27 | background?: string; 28 | } 29 | 30 | export interface LogInfo { 31 | updatedLog: string; 32 | sections: LogSection[]; 33 | colorFormats: LogColorInfo[]; 34 | } 35 | 36 | export function parseLog(log: string): LogInfo { 37 | let firstSection: LogSection | null = { 38 | name: "Setup", 39 | type: Type.Setup, 40 | start: 0, 41 | end: 1 42 | }; 43 | 44 | // Assume there is always the setup section 45 | const sections: LogSection[] = [firstSection]; 46 | 47 | const colorInfo: LogColorInfo[] = []; 48 | 49 | let currentRange: LogSection | null = null; 50 | const lines = log.split(/\n|\r/).filter(l => !!l); 51 | let lineIdx = 0; 52 | 53 | for (const line of lines) { 54 | // Groups 55 | const groupMarkerStart = line.indexOf(groupMarker); 56 | if (groupMarkerStart !== -1) { 57 | // If this is the first group marker we encounter, the previous range was the job setup 58 | if (firstSection) { 59 | firstSection.end = lineIdx - 1; 60 | firstSection = null; 61 | } 62 | 63 | if (currentRange) { 64 | currentRange.end = lineIdx - 1; 65 | sections.push(currentRange); 66 | } 67 | 68 | const name = line.substr(groupMarkerStart + groupMarker.length); 69 | 70 | currentRange = { 71 | name, 72 | type: Type.Step, 73 | start: lineIdx, 74 | end: lineIdx + 1 75 | }; 76 | } 77 | 78 | // Remove commands 79 | lines[lineIdx] = line.replace(commandRE, ""); 80 | 81 | // Check for custom colors 82 | let match: RegExpExecArray | null; 83 | if ((match = ansiColorRE.exec(line))) { 84 | const colorConfig = match[1]; 85 | const text = match[2]; 86 | 87 | colorInfo.push({ 88 | line: lineIdx, 89 | color: parseCustomColor(colorConfig), 90 | start: match.index, 91 | end: match.index + text.length 92 | }); 93 | 94 | // Remove from output 95 | lines[lineIdx] = line.replace(ansiColorRE, text); 96 | } 97 | 98 | ++lineIdx; 99 | } 100 | 101 | if (currentRange) { 102 | currentRange.end = lineIdx - 1; 103 | sections.push(currentRange); 104 | } 105 | 106 | return { 107 | updatedLog: lines.join("\n"), 108 | sections, 109 | colorFormats: colorInfo 110 | }; 111 | } 112 | 113 | function parseCustomColor(str: string): CustomColor { 114 | const ret: CustomColor = {}; 115 | 116 | const segments = str.split(";"); 117 | if (segments.length > 0) { 118 | ret.foreground = segments[0]; 119 | } 120 | 121 | if (segments.length > 1) { 122 | ret.background = segments[1]; 123 | } 124 | 125 | return ret; 126 | } 127 | -------------------------------------------------------------------------------- /src/logs/scheme.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { LogScheme } from "./constants"; 3 | 4 | /** 5 | * @param displayName Must not contain '/' 6 | */ 7 | export function buildLogURI( 8 | displayName: string, 9 | owner: string, 10 | repo: string, 11 | jobId: number 12 | ): vscode.Uri { 13 | return vscode.Uri.parse( 14 | `${LogScheme}://${owner}/${repo}/${displayName}?${jobId}` 15 | ); 16 | } 17 | 18 | export function parseUri(uri: vscode.Uri): { 19 | owner: string; 20 | repo: string; 21 | jobId: number; 22 | stepName?: string; 23 | } { 24 | if (uri.scheme != LogScheme) { 25 | throw new Error("Uri is not of log scheme"); 26 | } 27 | 28 | return { 29 | owner: uri.authority, 30 | repo: uri.path.split("/").slice(0, 2).join(""), 31 | jobId: parseInt(uri.query, 10), 32 | stepName: uri.fragment, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/logs/symbolProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { getLogInfo } from "./logInfoProvider"; 3 | 4 | export class WorkflowStepLogSymbolProvider 5 | implements vscode.DocumentSymbolProvider { 6 | provideDocumentSymbols( 7 | document: vscode.TextDocument, 8 | token: vscode.CancellationToken 9 | ): vscode.ProviderResult< 10 | vscode.SymbolInformation[] | vscode.DocumentSymbol[] 11 | > { 12 | const logInfo = getLogInfo(document.uri); 13 | if (!logInfo) { 14 | return []; 15 | } 16 | 17 | return logInfo.sections.map( 18 | s => 19 | new vscode.DocumentSymbol( 20 | s.name || "Setup", 21 | "Step", 22 | vscode.SymbolKind.Function, 23 | new vscode.Range(s.start, 0, s.end, 0), 24 | new vscode.Range(s.start, 0, s.end, 0) 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/model.ts: -------------------------------------------------------------------------------- 1 | import { RestEndpointMethods } from "@octokit/plugin-rest-endpoint-methods/dist-types/generated/method-types"; 2 | 3 | // Type helpers 4 | type Await = T extends { 5 | then(onfulfilled?: (value: infer U) => unknown): unknown; 6 | } 7 | ? U 8 | : T; 9 | 10 | type GetElementType = T extends (infer U)[] ? U : never; 11 | 12 | type OctokitData< 13 | Operation extends keyof RestEndpointMethods["actions"], 14 | ResultProperty extends keyof Await< 15 | ReturnType 16 | >["data"] 17 | > = GetElementType< 18 | Await< 19 | ReturnType 20 | >["data"][ResultProperty] 21 | >; 22 | 23 | type OctokitRepoData< 24 | Operation extends keyof RestEndpointMethods["repos"], 25 | ResultProperty extends keyof Await< 26 | ReturnType 27 | >["data"] 28 | > = GetElementType< 29 | Await< 30 | ReturnType 31 | >["data"][ResultProperty] 32 | >; 33 | 34 | // 35 | // Domain types 36 | // 37 | 38 | export type Workflow = OctokitData<"listRepoWorkflows", "workflows">; 39 | export type WorkflowRun = OctokitData<"listWorkflowRuns", "workflow_runs"> & { 40 | conclusion: string | null; 41 | }; 42 | 43 | export type WorkflowJob = OctokitData<"listJobsForWorkflowRun", "jobs">; 44 | 45 | export type WorkflowStep = GetElementType; 46 | 47 | export type RepoSecret = OctokitData<"listRepoSecrets", "secrets">; 48 | 49 | export type OrgSecret = OctokitData<"listOrgSecrets", "secrets">; 50 | 51 | export type Environment = OctokitRepoData<"getAllEnvironments", "environments">; 52 | 53 | export type EnvironmentSecret = OctokitData< 54 | "listEnvironmentSecrets", 55 | "secrets" 56 | >; 57 | 58 | export type SelfHostedRunner = OctokitData< 59 | "listSelfHostedRunnersForRepo", 60 | "runners" 61 | >; 62 | -------------------------------------------------------------------------------- /src/pinnedWorkflows/pinnedWorkflows.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { 4 | GitHubRepoContext, 5 | getGitHubContextForWorkspaceUri, 6 | } from "../git/repository"; 7 | import { 8 | getPinnedWorkflows, 9 | isPinnedWorkflowsRefreshEnabled, 10 | onPinnedWorkflowsChange, 11 | pinnedWorkflowsRefreshInterval, 12 | } from "../configuration/configuration"; 13 | 14 | import { WorkflowRun } from "../model"; 15 | import { getCodIconForWorkflowrun } from "../treeViews/icons"; 16 | import { sep } from "path"; 17 | 18 | interface PinnedWorkflow { 19 | /** Displayed name */ 20 | workflowName: string; 21 | 22 | workflowId: string; 23 | 24 | gitHubRepoContext: GitHubRepoContext; 25 | 26 | /** Status bar item created for this workflow */ 27 | statusBarItem: vscode.StatusBarItem; 28 | } 29 | 30 | const pinnedWorkflows: PinnedWorkflow[] = []; 31 | let refreshTimer: /*NodeJS.Timeout*/ any | undefined; 32 | 33 | export async function initPinnedWorkflows(context: vscode.ExtensionContext) { 34 | // Register handler for configuration changes 35 | onPinnedWorkflowsChange(_init); 36 | 37 | await _init(); 38 | } 39 | 40 | async function _init() { 41 | await updatePinnedWorkflows(); 42 | 43 | if (refreshTimer) { 44 | clearInterval(refreshTimer); 45 | refreshTimer = undefined; 46 | } 47 | if (isPinnedWorkflowsRefreshEnabled()) { 48 | refreshTimer = setInterval( 49 | refreshPinnedWorkflows, 50 | pinnedWorkflowsRefreshInterval() * 1000 51 | ); 52 | } 53 | } 54 | 55 | async function updatePinnedWorkflows() { 56 | clearPinnedWorkflows(); 57 | const pinnedWorkflows = getPinnedWorkflows(); 58 | 59 | // Assume we have a folder open. Without a folder open, we can't do anything 60 | if (!vscode.workspace.workspaceFolders?.length) { 61 | return; 62 | } 63 | 64 | const firstWorkspaceFolderName = vscode.workspace.workspaceFolders[0].name; 65 | 66 | let workflowsByWorkspace = new Map(); 67 | 68 | for (const pinnedWorkflow of pinnedWorkflows) { 69 | let workflowPath = pinnedWorkflow; 70 | if (pinnedWorkflow.startsWith(".github/")) { 71 | // No workspace, attribute to the first workspace folder 72 | workflowsByWorkspace.set(firstWorkspaceFolderName, [ 73 | pinnedWorkflow, 74 | ...(workflowsByWorkspace.get(firstWorkspaceFolderName) || []), 75 | ]); 76 | } else { 77 | const [workSpaceName, ...r] = workflowPath.split(sep); 78 | workflowsByWorkspace.set(workSpaceName, [ 79 | r.join(sep), 80 | ...(workflowsByWorkspace.get(workSpaceName) || []), 81 | ]); 82 | } 83 | } 84 | 85 | for (const workspaceName of workflowsByWorkspace.keys()) { 86 | const workspace = vscode.workspace.workspaceFolders?.find( 87 | (x) => x.name === workspaceName 88 | ); 89 | if (!workspace) { 90 | continue; 91 | } 92 | 93 | const gitHubRepoContext = await getGitHubContextForWorkspaceUri( 94 | workspace.uri 95 | ); 96 | if (!gitHubRepoContext) { 97 | return; 98 | } 99 | 100 | // Get all workflows to resolve names. We could do this locally, but for now, let's make the API call. 101 | const workflows = await gitHubRepoContext.client.actions.listRepoWorkflows({ 102 | owner: gitHubRepoContext.owner, 103 | repo: gitHubRepoContext.name, 104 | }); 105 | const workflowNameByPath: { [id: string]: string } = {}; 106 | workflows.data.workflows.forEach( 107 | (w) => (workflowNameByPath[w.path] = w.name) 108 | ); 109 | for (const pinnedWorkflow of workflowsByWorkspace.get(workspaceName) || 110 | []) { 111 | const pW = createPinnedWorkflow( 112 | gitHubRepoContext, 113 | pinnedWorkflow, 114 | workflowNameByPath[pinnedWorkflow] 115 | ); 116 | await updatePinnedWorkflow(pW); 117 | } 118 | } 119 | } 120 | 121 | async function refreshPinnedWorkflows() { 122 | for (const pinnedWorkflow of pinnedWorkflows) { 123 | await updatePinnedWorkflow(pinnedWorkflow); 124 | } 125 | } 126 | 127 | function clearPinnedWorkflows() { 128 | // Remove any existing pinned workflows 129 | for (const pinnedWorkflow of pinnedWorkflows) { 130 | pinnedWorkflow.statusBarItem.hide(); 131 | pinnedWorkflow.statusBarItem.dispose(); 132 | } 133 | 134 | pinnedWorkflows.splice(0, pinnedWorkflows.length); 135 | } 136 | 137 | function createPinnedWorkflow( 138 | gitHubRepoContext: GitHubRepoContext, 139 | id: string, 140 | name: string 141 | ): PinnedWorkflow { 142 | const statusBarItem = vscode.window.createStatusBarItem( 143 | vscode.StatusBarAlignment.Left 144 | ); 145 | 146 | const pinnedWorkflow = { 147 | gitHubRepoContext, 148 | workflowId: id, 149 | workflowName: name, 150 | statusBarItem, 151 | }; 152 | 153 | pinnedWorkflows.push(pinnedWorkflow); 154 | 155 | return pinnedWorkflow; 156 | } 157 | 158 | async function updatePinnedWorkflow(pinnedWorkflow: PinnedWorkflow) { 159 | const { gitHubRepoContext } = pinnedWorkflow; 160 | 161 | try { 162 | const runs = await gitHubRepoContext.client.actions.listWorkflowRuns({ 163 | owner: gitHubRepoContext.owner, 164 | repo: gitHubRepoContext.name, 165 | workflow_id: pinnedWorkflow.workflowId as any, // Workflow can also be a file name 166 | per_page: 1, 167 | }); 168 | const { total_count, workflow_runs } = runs.data; 169 | if (total_count == 0) { 170 | // Workflow has never run, set default text 171 | pinnedWorkflow.statusBarItem.text = `$(${getCodIconForWorkflowrun()}) ${ 172 | pinnedWorkflow.workflowName 173 | }`; 174 | // Can't do anything without a run 175 | pinnedWorkflow.statusBarItem.command = undefined; 176 | } 177 | const mostRecentRun = workflow_runs[0] as WorkflowRun; 178 | pinnedWorkflow.statusBarItem.text = `$(${getCodIconForWorkflowrun( 179 | mostRecentRun 180 | )}) ${pinnedWorkflow.workflowName}`; 181 | if (mostRecentRun.conclusion === "failure") { 182 | pinnedWorkflow.statusBarItem.backgroundColor = new vscode.ThemeColor( 183 | "statusBarItem.errorBackground" 184 | ); 185 | } else { 186 | pinnedWorkflow.statusBarItem.backgroundColor = undefined; 187 | } 188 | pinnedWorkflow.statusBarItem.command = { 189 | title: "Open workflow run", 190 | command: "github-actions.workflow.run.open", 191 | arguments: [ 192 | { 193 | run: mostRecentRun, 194 | }, 195 | ], 196 | }; 197 | // TODO: Do we need to hide before? 198 | pinnedWorkflow.statusBarItem.show(); 199 | } catch { 200 | // TODO: Display error 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/secrets/index.ts: -------------------------------------------------------------------------------- 1 | import sodium = require("tweetsodium"); 2 | import util = require("util"); 3 | import { atob, btoa } from "abab"; 4 | 5 | function decode(encoded: string): Uint8Array { 6 | const bytes = atob(encoded)! 7 | .split("") 8 | .map((x: string) => x.charCodeAt(0)); 9 | return Uint8Array.from(bytes); 10 | } 11 | 12 | function encode(bytes: Uint8Array): string { 13 | return btoa(String.fromCharCode.apply(null, Array.from(bytes)))!; 14 | } 15 | 16 | export function encodeSecret(key: string, value: string): string { 17 | const encoder = new util.TextEncoder(); 18 | // Convert the message and key to Uint8Array's 19 | const messageBytes = encoder.encode(value); 20 | const keyBytes = decode(key); 21 | 22 | // Encrypt using LibSodium. 23 | const encryptedBytes = sodium.seal(messageBytes, keyBytes); 24 | 25 | // Base64 the encrypted secret 26 | return encode(encryptedBytes); 27 | } 28 | -------------------------------------------------------------------------------- /src/tracker/workflowDocumentTracker.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { extname } from "path"; 4 | import { getContextStringForWorkflow } from "../workflow/workflow"; 5 | 6 | export function initWorkflowDocumentTracking(context: vscode.ExtensionContext) { 7 | context.subscriptions.push( 8 | vscode.window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor) 9 | ); 10 | 11 | // Check for initial document 12 | onDidChangeActiveTextEditor(vscode.window.activeTextEditor); 13 | } 14 | 15 | async function onDidChangeActiveTextEditor(editor?: vscode.TextEditor) { 16 | if (!editor || !isTextEditor(editor)) { 17 | return; 18 | } 19 | 20 | // Check if the file is saved and could be a workflow 21 | if ( 22 | !editor.document.uri?.fsPath || 23 | editor.document.uri.scheme !== "file" || 24 | (extname(editor.document.fileName) !== ".yaml" && 25 | extname(editor.document.fileName) !== ".yml") || 26 | editor.document.fileName.indexOf(".github/workflows") === -1 27 | ) { 28 | return; 29 | } 30 | 31 | vscode.commands.executeCommand( 32 | "setContext", 33 | "githubActions:activeFile", 34 | await getContextStringForWorkflow(editor.document.fileName) 35 | ); 36 | } 37 | 38 | // Adapted from from https://github.com/eamodio/vscode-gitlens/blob/f22a9cd4199ac498c217643282a6a412e1fc01ae/src/constants.ts#L74 39 | enum DocumentSchemes { 40 | DebugConsole = "debug", 41 | Output = "output", 42 | } 43 | 44 | function isTextEditor(editor: vscode.TextEditor): boolean { 45 | const scheme = editor.document.uri.scheme; 46 | return ( 47 | scheme !== DocumentSchemes.Output && scheme !== DocumentSchemes.DebugConsole 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/treeViews/current-branch/currentBranchRepoNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { GitHubRepoContext, getCurrentBranch } from "../../git/repository"; 4 | 5 | import { NoRunForBranchNode } from "./noRunForBranchNode"; 6 | import { WorkflowRunNode } from "../workflows/workflowRunNode"; 7 | import { logDebug } from "../../log"; 8 | 9 | export class CurrentBranchRepoNode extends vscode.TreeItem { 10 | constructor( 11 | public readonly gitHubRepoContext: GitHubRepoContext, 12 | public readonly currentBranchName: string 13 | ) { 14 | super(gitHubRepoContext.name, vscode.TreeItemCollapsibleState.Collapsed); 15 | 16 | this.description = currentBranchName; 17 | this.contextValue = "cb-repo"; 18 | } 19 | 20 | async getRuns(): Promise<(WorkflowRunNode | NoRunForBranchNode)[]> { 21 | logDebug("Getting workflow runs for current branch"); 22 | 23 | return ( 24 | (await getCurrentBranchWorkflowRunNodes(this.gitHubRepoContext)) || [] 25 | ); 26 | } 27 | } 28 | 29 | export async function getCurrentBranchWorkflowRunNodes( 30 | gitHubRepoContext: GitHubRepoContext 31 | ): Promise<(WorkflowRunNode | NoRunForBranchNode)[] | undefined> { 32 | const currentBranch = getCurrentBranch(gitHubRepoContext.repositoryState); 33 | if (!currentBranch) { 34 | logDebug(`Could not find current branch for ${gitHubRepoContext.name}`); 35 | return []; 36 | } 37 | 38 | const result = await gitHubRepoContext.client.actions.listWorkflowRunsForRepo( 39 | { 40 | owner: gitHubRepoContext.owner, 41 | repo: gitHubRepoContext.name, 42 | branch: currentBranch, 43 | } 44 | ); 45 | 46 | const resp = result.data; 47 | const runs = resp.workflow_runs; 48 | 49 | if (runs?.length == 0) { 50 | return [new NoRunForBranchNode()]; 51 | } 52 | 53 | return runs.map((wr) => { 54 | const wf = wr.workflow_id; 55 | 56 | // TODO: Do we need to include the workflow name here? 57 | return new WorkflowRunNode(gitHubRepoContext, wr, wr.name ?? undefined); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/treeViews/current-branch/noRunForBranchNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class NoRunForBranchNode extends vscode.TreeItem { 4 | constructor() { 5 | super("No runs for current branch"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/treeViews/currentBranch.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { 4 | CurrentBranchRepoNode, 5 | getCurrentBranchWorkflowRunNodes, 6 | } from "./current-branch/currentBranchRepoNode"; 7 | import { getCurrentBranch, getGitHubContext } from "../git/repository"; 8 | 9 | import { NoRunForBranchNode } from "./current-branch/noRunForBranchNode"; 10 | import { WorkflowJobNode } from "./workflows/workflowJobNode"; 11 | import { WorkflowRunNode } from "./workflows/workflowRunNode"; 12 | import { WorkflowStepNode } from "./workflows/workflowStepNode"; 13 | import { logDebug } from "../log"; 14 | 15 | type CurrentBranchTreeNode = 16 | | CurrentBranchRepoNode 17 | | WorkflowRunNode 18 | | WorkflowJobNode 19 | | WorkflowStepNode 20 | | NoRunForBranchNode; 21 | 22 | export class CurrentBranchTreeProvider 23 | implements vscode.TreeDataProvider 24 | { 25 | private _onDidChangeTreeData = 26 | new vscode.EventEmitter(); 27 | readonly onDidChangeTreeData = this._onDidChangeTreeData.event; 28 | 29 | refresh(): void { 30 | this._onDidChangeTreeData.fire(null); 31 | } 32 | 33 | getTreeItem( 34 | element: CurrentBranchTreeNode 35 | ): vscode.TreeItem | Thenable { 36 | return element; 37 | } 38 | 39 | async getChildren( 40 | element?: CurrentBranchTreeNode | undefined 41 | ): Promise { 42 | if (!element) { 43 | const gitHubContext = await getGitHubContext(); 44 | if (!gitHubContext) { 45 | return []; 46 | } 47 | 48 | if (gitHubContext.repos.length === 1) { 49 | return ( 50 | (await getCurrentBranchWorkflowRunNodes(gitHubContext.repos[0])) || [] 51 | ); 52 | } 53 | 54 | if (gitHubContext.repos.length > 1) { 55 | return gitHubContext.repos 56 | .map((repoContext): CurrentBranchRepoNode | undefined => { 57 | const currentBranch = getCurrentBranch(repoContext.repositoryState); 58 | if (!currentBranch) { 59 | logDebug(`Could not find current branch for ${repoContext.name}`); 60 | return undefined; 61 | } 62 | 63 | return new CurrentBranchRepoNode(repoContext, currentBranch); 64 | }) 65 | .filter((x) => x !== undefined) as CurrentBranchRepoNode[]; 66 | } 67 | } else if (element instanceof CurrentBranchRepoNode) { 68 | return element.getRuns(); 69 | } else if (element instanceof WorkflowRunNode) { 70 | return element.getJobs(); 71 | } else if (element instanceof WorkflowJobNode) { 72 | return element.getSteps(); 73 | } 74 | 75 | return []; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/treeViews/icons.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | let _context: vscode.ExtensionContext; 4 | export function initResources(context: vscode.ExtensionContext) { 5 | _context = context; 6 | } 7 | 8 | export interface IStatusAndConclusion { 9 | status: string | null; 10 | conclusion: string | null; 11 | } 12 | 13 | export function getAbsoluteIconPath(relativeIconPath: string): { 14 | light: string | vscode.Uri; 15 | dark: string | vscode.Uri; 16 | } { 17 | return { 18 | light: _context.asAbsolutePath(`resources/icons/light/${relativeIconPath}`), 19 | dark: _context.asAbsolutePath(`resources/icons/dark/${relativeIconPath}`), 20 | }; 21 | } 22 | 23 | export function getIconForWorkflowRun(runOrJob: IStatusAndConclusion) { 24 | return _getIconForWorkflowrun(runOrJob); 25 | } 26 | 27 | function _getIconForWorkflowrun( 28 | runOrJob: IStatusAndConclusion 29 | ): 30 | | string 31 | | vscode.ThemeIcon 32 | | { light: string | vscode.Uri; dark: string | vscode.Uri } { 33 | switch (runOrJob.status) { 34 | case "completed": { 35 | switch (runOrJob.conclusion) { 36 | case "success": 37 | return getAbsoluteIconPath("conclusions/success.svg"); 38 | 39 | case "failure": 40 | return getAbsoluteIconPath("conclusions/failure.svg"); 41 | 42 | case "cancelled": 43 | return getAbsoluteIconPath("conclusions/cancelled.svg"); 44 | } 45 | } 46 | 47 | case "queued": 48 | return getAbsoluteIconPath("statuses/queued.svg"); 49 | 50 | case "waiting": 51 | return getAbsoluteIconPath("statuses/waiting.svg"); 52 | 53 | case "inprogress": 54 | case "in_progress": 55 | return new vscode.ThemeIcon("sync~spin"); 56 | } 57 | 58 | return ""; 59 | } 60 | 61 | /** Get one of the built-in VS Code icons */ 62 | export function getCodIconForWorkflowrun( 63 | runOrJob?: IStatusAndConclusion 64 | ): string { 65 | if (!runOrJob) { 66 | return "circle-outline"; 67 | } 68 | 69 | switch (runOrJob.status) { 70 | case "completed": { 71 | switch (runOrJob.conclusion) { 72 | case "success": 73 | return "pass"; 74 | 75 | case "failure": 76 | return "error"; 77 | 78 | case "cancelled": 79 | return "circle-slash"; 80 | } 81 | } 82 | 83 | case "queued": 84 | return "primitive-dot"; 85 | 86 | case "waiting": 87 | return "bell"; 88 | 89 | case "inprogress": 90 | case "in_progress": 91 | return "sync~spin"; 92 | } 93 | 94 | return ""; 95 | } 96 | -------------------------------------------------------------------------------- /src/treeViews/settings.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { SettingsRepoNode, getSettingNodes } from "./settings/settingsRepoNode"; 4 | 5 | import { EnvironmentNode } from "./settings/environmentNode"; 6 | import { EnvironmentsNode } from "./settings/environmentsNode"; 7 | import { OrgSecretsNode } from "./settings/orgSecretsNode"; 8 | import { RepoSecretsNode } from "./settings/repoSecretsNode"; 9 | import { SecretsNode } from "./settings/secretsNode"; 10 | import { SelfHostedRunnersNode } from "./settings/selfHostedRunnersNode"; 11 | import { SettingsExplorerNode } from "./settings/types"; 12 | import { getGitHubContext } from "../git/repository"; 13 | 14 | export class SettingsTreeProvider 15 | implements vscode.TreeDataProvider 16 | { 17 | private _onDidChangeTreeData = 18 | new vscode.EventEmitter(); 19 | readonly onDidChangeTreeData = this._onDidChangeTreeData.event; 20 | 21 | refresh(): void { 22 | this._onDidChangeTreeData.fire(null); 23 | } 24 | 25 | getTreeItem( 26 | element: SettingsExplorerNode 27 | ): vscode.TreeItem | Thenable { 28 | return element; 29 | } 30 | 31 | async getChildren( 32 | element?: SettingsExplorerNode | undefined 33 | ): Promise { 34 | const gitHubContext = await getGitHubContext(); 35 | if (!gitHubContext) { 36 | return []; 37 | } 38 | 39 | if (!element) { 40 | if (gitHubContext.repos.length > 0) { 41 | if (gitHubContext.repos.length == 1) { 42 | return getSettingNodes(gitHubContext.repos[0]); 43 | } 44 | 45 | return gitHubContext.repos.map((r) => new SettingsRepoNode(r)); 46 | } 47 | } 48 | 49 | if (element instanceof SettingsRepoNode) { 50 | return element.getSettings(); 51 | } 52 | 53 | // 54 | // Secrets 55 | // 56 | if (element instanceof SecretsNode) { 57 | const nodes = [new RepoSecretsNode(element.gitHubRepoContext)]; 58 | 59 | if (element.gitHubRepoContext.ownerIsOrg) { 60 | nodes.push(new OrgSecretsNode(element.gitHubRepoContext)); 61 | } 62 | 63 | return nodes; 64 | } 65 | 66 | if (element instanceof RepoSecretsNode) { 67 | return element.getSecrets(); 68 | } 69 | 70 | if (element instanceof OrgSecretsNode) { 71 | return element.getSecrets(); 72 | } 73 | 74 | if (element instanceof SelfHostedRunnersNode) { 75 | return element.getRunners(); 76 | } 77 | 78 | // 79 | // Environments 80 | // 81 | 82 | if (element instanceof EnvironmentsNode) { 83 | return element.getEnvironments(); 84 | } 85 | 86 | if (element instanceof EnvironmentNode) { 87 | return element.getSecrets(); 88 | } 89 | 90 | return []; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/treeViews/settings/emptyEnvironmentSecretsNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class EmptyEnvironmentSecretsNode extends vscode.TreeItem { 4 | constructor() { 5 | super("No environment secrets defined"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/treeViews/settings/environmentNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { Environment } from "../../model"; 4 | import { EmptyEnvironmentSecretsNode } from "./emptyEnvironmentSecretsNode"; 5 | import { EnvironmentSecretNode } from "./environmentSecretNode"; 6 | 7 | export class EnvironmentNode extends vscode.TreeItem { 8 | constructor( 9 | public readonly gitHubRepoContext: GitHubRepoContext, 10 | public readonly environment: Environment 11 | ) { 12 | super(environment.name, vscode.TreeItemCollapsibleState.Collapsed); 13 | 14 | this.contextValue = "environment"; 15 | } 16 | 17 | async getSecrets(): Promise< 18 | (EnvironmentSecretNode | EmptyEnvironmentSecretsNode)[] 19 | > { 20 | const result = 21 | await this.gitHubRepoContext.client.actions.listEnvironmentSecrets({ 22 | repository_id: this.gitHubRepoContext.id, 23 | environment_name: this.environment.name, 24 | }); 25 | 26 | const data = result.data.secrets; 27 | if (!data) { 28 | return [new EmptyEnvironmentSecretsNode()]; 29 | } 30 | 31 | return data.map( 32 | (s) => new EnvironmentSecretNode(this.gitHubRepoContext, s) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/treeViews/settings/environmentSecretNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { EnvironmentSecret } from "../../model"; 4 | 5 | export class EnvironmentSecretNode extends vscode.TreeItem { 6 | constructor( 7 | public readonly gitHubRepoContext: GitHubRepoContext, 8 | public readonly secret: EnvironmentSecret 9 | ) { 10 | super(secret.name); 11 | 12 | this.contextValue = "env-secret"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/treeViews/settings/environmentsNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { EnvironmentNode } from "./environmentNode"; 4 | 5 | export class EnvironmentsNode extends vscode.TreeItem { 6 | constructor(public readonly gitHubRepoContext: GitHubRepoContext) { 7 | super("Environments", vscode.TreeItemCollapsibleState.Collapsed); 8 | 9 | this.iconPath = new vscode.ThemeIcon("server-environment"); 10 | } 11 | 12 | async getEnvironments(): Promise { 13 | const result = await this.gitHubRepoContext.client.repos.getAllEnvironments( 14 | { 15 | owner: this.gitHubRepoContext.owner, 16 | repo: this.gitHubRepoContext.name, 17 | } 18 | ); 19 | 20 | const data = result.data.environments || []; 21 | return data.map((e) => new EnvironmentNode(this.gitHubRepoContext, e)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/treeViews/settings/orgFeaturesNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class OrgFeaturesNode extends vscode.TreeItem { 4 | constructor() { 5 | super("GitHub token does not have `admin:org` scope"); 6 | this.description = "Click here to authorize"; 7 | 8 | this.command = { 9 | title: "Login", 10 | command: "github-actions.auth.org-login", 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/treeViews/settings/orgSecretNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { OrgSecret } from "../../model"; 4 | 5 | export class OrgSecretNode extends vscode.TreeItem { 6 | constructor( 7 | public readonly gitHubRepoContext: GitHubRepoContext, 8 | public readonly secret: OrgSecret 9 | ) { 10 | super(secret.name); 11 | 12 | this.description = this.secret.visibility; 13 | this.contextValue = "org-secret"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/treeViews/settings/orgSecretsNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { OrgFeaturesNode } from "./orgFeaturesNode"; 4 | import { OrgSecretNode } from "./orgSecretNode"; 5 | 6 | export class OrgSecretsNode extends vscode.TreeItem { 7 | constructor(public readonly gitHubRepoContext: GitHubRepoContext) { 8 | super("Organization Secrets", vscode.TreeItemCollapsibleState.Collapsed); 9 | 10 | this.contextValue = "org-secrets"; 11 | } 12 | 13 | async getSecrets(): Promise { 14 | if (!this.gitHubRepoContext.orgFeaturesEnabled) { 15 | return [new OrgFeaturesNode()]; 16 | } 17 | 18 | const result = await this.gitHubRepoContext.client.actions.listOrgSecrets({ 19 | org: this.gitHubRepoContext.owner, 20 | }); 21 | 22 | return result.data.secrets.map( 23 | (s) => new OrgSecretNode(this.gitHubRepoContext, s) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/treeViews/settings/repoSecretNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { RepoSecret } from "../../model"; 4 | 5 | export class RepoSecretNode extends vscode.TreeItem { 6 | constructor( 7 | public readonly gitHubRepoContext: GitHubRepoContext, 8 | public readonly secret: RepoSecret 9 | ) { 10 | super(secret.name); 11 | 12 | this.contextValue = "secret"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/treeViews/settings/repoSecretsNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { RepoSecretNode } from "./repoSecretNode"; 4 | 5 | export class RepoSecretsNode extends vscode.TreeItem { 6 | constructor(public readonly gitHubRepoContext: GitHubRepoContext) { 7 | super("Repository Secrets", vscode.TreeItemCollapsibleState.Collapsed); 8 | 9 | this.contextValue = "secrets"; 10 | } 11 | 12 | async getSecrets(): Promise { 13 | const result = await this.gitHubRepoContext.client.actions.listRepoSecrets({ 14 | owner: this.gitHubRepoContext.owner, 15 | repo: this.gitHubRepoContext.name, 16 | }); 17 | 18 | return result.data.secrets.map( 19 | (s) => new RepoSecretNode(this.gitHubRepoContext, s) 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/treeViews/settings/secretsNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | 4 | export class SecretsNode extends vscode.TreeItem { 5 | constructor(public readonly gitHubRepoContext: GitHubRepoContext) { 6 | super("Secrets", vscode.TreeItemCollapsibleState.Collapsed); 7 | 8 | this.iconPath = new vscode.ThemeIcon("lock"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/treeViews/settings/selfHostedRunnerNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { SelfHostedRunner } from "../../model"; 4 | import { getAbsoluteIconPath } from "../icons"; 5 | 6 | export class SelfHostedRunnerNode extends vscode.TreeItem { 7 | constructor( 8 | public readonly gitHubRepoContext: GitHubRepoContext, 9 | public readonly selfHostedRunner: SelfHostedRunner 10 | ) { 11 | super(selfHostedRunner.name); 12 | 13 | this.contextValue = "runner"; 14 | this.tooltip = this.selfHostedRunner.status; 15 | this.iconPath = getAbsoluteIconPath( 16 | this.selfHostedRunner.status == "online" 17 | ? "runner-online.svg" 18 | : "runner-offline.svg" 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/treeViews/settings/selfHostedRunnersNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { SelfHostedRunnerNode } from "./selfHostedRunnerNode"; 4 | 5 | export class SelfHostedRunnersNode extends vscode.TreeItem { 6 | constructor(public readonly gitHubRepoContext: GitHubRepoContext) { 7 | super("Self-hosted runners", vscode.TreeItemCollapsibleState.Collapsed); 8 | 9 | this.contextValue = "runners"; 10 | this.iconPath = new vscode.ThemeIcon("server"); 11 | } 12 | 13 | async getRunners(): Promise { 14 | const result = 15 | await this.gitHubRepoContext.client.actions.listSelfHostedRunnersForRepo({ 16 | owner: this.gitHubRepoContext.owner, 17 | repo: this.gitHubRepoContext.name, 18 | }); 19 | 20 | const data = result.data.runners || []; 21 | return data.map((r) => new SelfHostedRunnerNode(this.gitHubRepoContext, r)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/treeViews/settings/settingsRepoNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { EnvironmentsNode } from "./environmentsNode"; 4 | import { GitHubRepoContext } from "../../git/repository"; 5 | import { SecretsNode } from "./secretsNode"; 6 | import { SelfHostedRunnersNode } from "./selfHostedRunnersNode"; 7 | import { SettingsExplorerNode } from "./types"; 8 | 9 | export class SettingsRepoNode extends vscode.TreeItem { 10 | constructor(public readonly gitHubRepoContext: GitHubRepoContext) { 11 | super(gitHubRepoContext.name, vscode.TreeItemCollapsibleState.Collapsed); 12 | 13 | this.contextValue = "settings-repo"; 14 | } 15 | 16 | async getSettings(): Promise { 17 | return getSettingNodes(this.gitHubRepoContext); 18 | } 19 | } 20 | 21 | export function getSettingNodes( 22 | gitHubContext: GitHubRepoContext 23 | ): SettingsExplorerNode[] { 24 | const nodes: SettingsExplorerNode[] = []; 25 | 26 | nodes.push(new EnvironmentsNode(gitHubContext)); 27 | nodes.push(new SecretsNode(gitHubContext)); 28 | nodes.push(new SelfHostedRunnersNode(gitHubContext)); 29 | 30 | return nodes; 31 | } 32 | -------------------------------------------------------------------------------- /src/treeViews/settings/types.ts: -------------------------------------------------------------------------------- 1 | import { EmptyEnvironmentSecretsNode } from "./emptyEnvironmentSecretsNode"; 2 | import { EnvironmentNode } from "./environmentNode"; 3 | import { EnvironmentSecretNode } from "./environmentSecretNode"; 4 | import { EnvironmentsNode } from "./environmentsNode"; 5 | import { OrgFeaturesNode } from "./orgFeaturesNode"; 6 | import { OrgSecretNode } from "./orgSecretNode"; 7 | import { RepoSecretNode } from "./repoSecretNode"; 8 | import { SecretsNode } from "./secretsNode"; 9 | import { SelfHostedRunnersNode } from "./selfHostedRunnersNode"; 10 | 11 | export type SettingsExplorerNode = 12 | | OrgFeaturesNode 13 | | SelfHostedRunnersNode 14 | | SecretsNode 15 | | RepoSecretNode 16 | | OrgSecretNode 17 | | EnvironmentsNode 18 | | EnvironmentNode 19 | | EnvironmentSecretNode 20 | | EmptyEnvironmentSecretsNode; 21 | -------------------------------------------------------------------------------- /src/treeViews/shared/authenticationNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class AuthenticationNode extends vscode.TreeItem { 4 | constructor() { 5 | super("Please sign-in in the Accounts menu."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/treeViews/shared/errorNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class ErrorNode extends vscode.TreeItem { 4 | constructor(message: string) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/treeViews/shared/noGitHubRepositoryNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** 4 | * When no github.com remote can be found in the current workspace. 5 | */ 6 | export class NoGitHubRepositoryNode extends vscode.TreeItem { 7 | constructor() { 8 | super("Did not find a github.com repository"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/treeViews/workflows.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { 4 | WorkflowsRepoNode, 5 | getWorkflowNodes, 6 | } from "./workflows/workflowsRepoNode"; 7 | import { log, logDebug, logError } from "../log"; 8 | 9 | import { AuthenticationNode } from "./shared/authenticationNode"; 10 | import { ErrorNode } from "./shared/errorNode"; 11 | import { NoGitHubRepositoryNode } from "./shared/noGitHubRepositoryNode"; 12 | import { WorkflowJobNode } from "./workflows/workflowJobNode"; 13 | import { WorkflowNode } from "./workflows/workflowNode"; 14 | import { WorkflowRunNode } from "./workflows/workflowRunNode"; 15 | import { WorkflowStepNode } from "./workflows/workflowStepNode"; 16 | import { getGitHubContext } from "../git/repository"; 17 | 18 | type WorkflowsTreeNode = 19 | | AuthenticationNode 20 | | NoGitHubRepositoryNode 21 | | WorkflowNode 22 | | WorkflowRunNode 23 | | WorkflowStepNode; 24 | 25 | export class WorkflowsTreeProvider 26 | implements vscode.TreeDataProvider 27 | { 28 | private _onDidChangeTreeData = 29 | new vscode.EventEmitter(); 30 | readonly onDidChangeTreeData = this._onDidChangeTreeData.event; 31 | 32 | refresh(): void { 33 | logDebug("Refreshing workflow tree"); 34 | this._onDidChangeTreeData.fire(null); 35 | } 36 | 37 | getTreeItem( 38 | element: WorkflowsTreeNode 39 | ): vscode.TreeItem | Thenable { 40 | return element; 41 | } 42 | 43 | async getChildren( 44 | element?: WorkflowsTreeNode | undefined 45 | ): Promise { 46 | logDebug("Getting root children"); 47 | 48 | if (!element) { 49 | try { 50 | const gitHubContext = await getGitHubContext(); 51 | if (!gitHubContext) { 52 | logDebug("could not get github context"); 53 | return []; 54 | } 55 | 56 | if (gitHubContext.repos.length > 0) { 57 | if (gitHubContext.repos.length == 1) { 58 | return getWorkflowNodes(gitHubContext.repos[0]); 59 | } 60 | 61 | return gitHubContext.repos.map((r) => new WorkflowsRepoNode(r)); 62 | } 63 | 64 | log("No GitHub repositories found"); 65 | return []; 66 | } catch (e: any) { 67 | logError(e as Error, "Failed to get GitHub context"); 68 | 69 | if ( 70 | `${e?.message}`.startsWith( 71 | "Could not get token from the GitHub authentication provider." 72 | ) 73 | ) { 74 | return [new AuthenticationNode()]; 75 | } 76 | 77 | return [new ErrorNode(`An error has occured: ${e.message}`)]; 78 | } 79 | } 80 | 81 | if (element instanceof WorkflowsRepoNode) { 82 | return element.getWorkflows(); 83 | } else if (element instanceof WorkflowNode) { 84 | return element.getRuns(); 85 | } else if (element instanceof WorkflowRunNode) { 86 | return element.getJobs(); 87 | } else if (element instanceof WorkflowJobNode) { 88 | return element.getSteps(); 89 | } 90 | 91 | return []; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/treeViews/workflows/workflowJobNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { WorkflowJob } from "../../model"; 4 | import { getIconForWorkflowRun } from "../icons"; 5 | import { WorkflowStepNode } from "./workflowStepNode"; 6 | 7 | export class WorkflowJobNode extends vscode.TreeItem { 8 | constructor( 9 | public readonly gitHubRepoContext: GitHubRepoContext, 10 | public readonly job: WorkflowJob 11 | ) { 12 | super( 13 | job.name, 14 | (job.steps && 15 | job.steps.length > 0 && 16 | vscode.TreeItemCollapsibleState.Collapsed) || 17 | undefined 18 | ); 19 | 20 | this.contextValue = "job"; 21 | if (this.job.status === "completed") { 22 | this.contextValue += " completed"; 23 | } 24 | 25 | this.iconPath = getIconForWorkflowRun(this.job); 26 | } 27 | 28 | hasSteps(): boolean { 29 | return !!(this.job.steps && this.job.steps.length > 0); 30 | } 31 | 32 | async getSteps(): Promise { 33 | return (this.job.steps || []).map( 34 | (s) => new WorkflowStepNode(this.gitHubRepoContext, this.job, s) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/treeViews/workflows/workflowNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { GitHubRepoContext } from "../../git/repository"; 4 | import { Workflow as ParsedWorkflow } from "github-actions-parser/dist/lib/workflow"; 5 | import { Workflow } from "../../model"; 6 | import { WorkflowRunNode } from "./workflowRunNode"; 7 | import { getPinnedWorkflows } from "../../configuration/configuration"; 8 | import { getWorkflowUri } from "../../workflow/workflow"; 9 | import { logDebug } from "../../log"; 10 | 11 | export class WorkflowNode extends vscode.TreeItem { 12 | constructor( 13 | public readonly gitHubRepoContext: GitHubRepoContext, 14 | public readonly wf: Workflow, 15 | public readonly parsed?: ParsedWorkflow 16 | ) { 17 | super(wf.name, vscode.TreeItemCollapsibleState.Collapsed); 18 | 19 | this.updateContextValue(); 20 | } 21 | 22 | updateContextValue() { 23 | this.contextValue = "workflow"; 24 | 25 | const workflowFullPath = getWorkflowUri( 26 | this.gitHubRepoContext, 27 | this.wf.path 28 | ); 29 | if (workflowFullPath) { 30 | const relativeWorkflowPath = 31 | vscode.workspace.asRelativePath(workflowFullPath); 32 | if (new Set(getPinnedWorkflows()).has(relativeWorkflowPath)) { 33 | this.contextValue += " pinned"; 34 | } else { 35 | this.contextValue += " pinnable"; 36 | } 37 | } 38 | 39 | if (this.parsed) { 40 | if (this.parsed.on.repository_dispatch !== undefined) { 41 | this.contextValue += " rdispatch"; 42 | } 43 | 44 | if (this.parsed.on.workflow_dispatch !== undefined) { 45 | this.contextValue += " wdispatch"; 46 | } 47 | } 48 | } 49 | 50 | async getRuns(): Promise { 51 | logDebug("Getting workflow runs"); 52 | 53 | const result = await this.gitHubRepoContext.client.actions.listWorkflowRuns( 54 | { 55 | owner: this.gitHubRepoContext.owner, 56 | repo: this.gitHubRepoContext.name, 57 | workflow_id: this.wf.id, 58 | } 59 | ); 60 | 61 | const resp = result.data; 62 | const runs = resp.workflow_runs; 63 | 64 | return runs.map((wr) => new WorkflowRunNode(this.gitHubRepoContext, wr)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/treeViews/workflows/workflowRunNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { WorkflowJob, WorkflowRun } from "../../model"; 4 | 5 | import { GitHubRepoContext } from "../../git/repository"; 6 | import { WorkflowJobNode } from "./workflowJobNode"; 7 | import { getIconForWorkflowRun } from "../icons"; 8 | import { logDebug } from "../../log"; 9 | 10 | export class WorkflowRunNode extends vscode.TreeItem { 11 | constructor( 12 | public readonly gitHubRepoContext: GitHubRepoContext, 13 | public readonly run: WorkflowRun, 14 | public readonly workflowName?: string 15 | ) { 16 | super( 17 | `${workflowName ? workflowName + " " : ""}#${run.id}`, 18 | vscode.TreeItemCollapsibleState.Collapsed 19 | ); 20 | 21 | this.description = `${run.event} (${(run.head_sha || "").substr(0, 7)})`; 22 | 23 | this.contextValue = "run"; 24 | if (this.run.status !== "completed") { 25 | this.contextValue += " cancelable"; 26 | } 27 | 28 | if (this.run.status === "completed" && this.run.conclusion !== "success") { 29 | this.contextValue += " rerunnable"; 30 | } 31 | 32 | if (this.run.status === "completed") { 33 | this.contextValue += "completed"; 34 | } 35 | 36 | this.iconPath = getIconForWorkflowRun(this.run); 37 | this.tooltip = `${this.run.status} ${this.run.conclusion || ""}`; 38 | } 39 | 40 | async getJobs(): Promise { 41 | logDebug("Getting workflow jobs"); 42 | 43 | const result = 44 | await this.gitHubRepoContext.client.actions.listJobsForWorkflowRun({ 45 | owner: this.gitHubRepoContext.owner, 46 | repo: this.gitHubRepoContext.name, 47 | run_id: this.run.id, 48 | }); 49 | 50 | const resp = result.data; 51 | const jobs: WorkflowJob[] = (resp as any).jobs; 52 | 53 | return jobs.map((job) => new WorkflowJobNode(this.gitHubRepoContext, job)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/treeViews/workflows/workflowStepNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { GitHubRepoContext } from "../../git/repository"; 3 | import { WorkflowJob, WorkflowStep } from "../../model"; 4 | import { getIconForWorkflowRun } from "../icons"; 5 | 6 | export class WorkflowStepNode extends vscode.TreeItem { 7 | constructor( 8 | public readonly gitHubRepoContext: GitHubRepoContext, 9 | public readonly job: WorkflowJob, 10 | public readonly step: WorkflowStep 11 | ) { 12 | super(step.name); 13 | 14 | this.contextValue = "step"; 15 | if (this.step.status === "completed") { 16 | this.contextValue += " completed"; 17 | } 18 | 19 | this.command = { 20 | title: "Open run", 21 | command: "github-actions.workflow.logs", 22 | arguments: [this], 23 | }; 24 | 25 | this.iconPath = getIconForWorkflowRun(this.step); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/treeViews/workflows/workflowsRepoNode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { getWorkflowUri, parseWorkflow } from "../../workflow/workflow"; 4 | 5 | import { GitHubRepoContext } from "../../git/repository"; 6 | import { Workflow as ParsedWorkflow } from "github-actions-parser/dist/lib/workflow"; 7 | import { WorkflowNode } from "./workflowNode"; 8 | import { logDebug } from "../../log"; 9 | 10 | export class WorkflowsRepoNode extends vscode.TreeItem { 11 | constructor(public readonly gitHubRepoContext: GitHubRepoContext) { 12 | super(gitHubRepoContext.name, vscode.TreeItemCollapsibleState.Collapsed); 13 | 14 | this.contextValue = "wf-repo"; 15 | } 16 | 17 | async getWorkflows(): Promise { 18 | logDebug("Getting workflows"); 19 | 20 | return getWorkflowNodes(this.gitHubRepoContext); 21 | } 22 | } 23 | 24 | export async function getWorkflowNodes(gitHubRepoContext: GitHubRepoContext) { 25 | const result = await gitHubRepoContext.client.actions.listRepoWorkflows({ 26 | owner: gitHubRepoContext.owner, 27 | repo: gitHubRepoContext.name, 28 | }); 29 | 30 | const resp = result.data; 31 | const workflows = resp.workflows; 32 | 33 | workflows.sort((a, b) => a.name.localeCompare(b.name)); 34 | 35 | return await Promise.all( 36 | workflows.map(async (wf) => { 37 | let parsedWorkflow: ParsedWorkflow | undefined; 38 | 39 | const workflowUri = getWorkflowUri(gitHubRepoContext, wf.path); 40 | if (workflowUri) { 41 | parsedWorkflow = await parseWorkflow(workflowUri, gitHubRepoContext); 42 | } 43 | 44 | return new WorkflowNode(gitHubRepoContext, wf, parsedWorkflow); 45 | }) 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/typings/git.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Disposable, Event, ProviderResult, Uri } from "vscode"; 7 | export { ProviderResult } from "vscode"; 8 | 9 | export interface Git { 10 | readonly path: string; 11 | } 12 | 13 | export interface InputBox { 14 | value: string; 15 | } 16 | 17 | export const enum ForcePushMode { 18 | Force, 19 | ForceWithLease, 20 | } 21 | 22 | export const enum RefType { 23 | Head, 24 | RemoteHead, 25 | Tag, 26 | } 27 | 28 | export interface Ref { 29 | readonly type: RefType; 30 | readonly name?: string; 31 | readonly commit?: string; 32 | readonly remote?: string; 33 | } 34 | 35 | export interface UpstreamRef { 36 | readonly remote: string; 37 | readonly name: string; 38 | } 39 | 40 | export interface Branch extends Ref { 41 | readonly upstream?: UpstreamRef; 42 | readonly ahead?: number; 43 | readonly behind?: number; 44 | } 45 | 46 | export interface Commit { 47 | readonly hash: string; 48 | readonly message: string; 49 | readonly parents: string[]; 50 | readonly authorDate?: Date; 51 | readonly authorName?: string; 52 | readonly authorEmail?: string; 53 | readonly commitDate?: Date; 54 | } 55 | 56 | export interface Submodule { 57 | readonly name: string; 58 | readonly path: string; 59 | readonly url: string; 60 | } 61 | 62 | export interface Remote { 63 | readonly name: string; 64 | readonly fetchUrl?: string; 65 | readonly pushUrl?: string; 66 | readonly isReadOnly: boolean; 67 | } 68 | 69 | export const enum Status { 70 | INDEX_MODIFIED, 71 | INDEX_ADDED, 72 | INDEX_DELETED, 73 | INDEX_RENAMED, 74 | INDEX_COPIED, 75 | 76 | MODIFIED, 77 | DELETED, 78 | UNTRACKED, 79 | IGNORED, 80 | INTENT_TO_ADD, 81 | 82 | ADDED_BY_US, 83 | ADDED_BY_THEM, 84 | DELETED_BY_US, 85 | DELETED_BY_THEM, 86 | BOTH_ADDED, 87 | BOTH_DELETED, 88 | BOTH_MODIFIED, 89 | } 90 | 91 | export interface Change { 92 | /** 93 | * Returns either `originalUri` or `renameUri`, depending 94 | * on whether this change is a rename change. When 95 | * in doubt always use `uri` over the other two alternatives. 96 | */ 97 | readonly uri: Uri; 98 | readonly originalUri: Uri; 99 | readonly renameUri: Uri | undefined; 100 | readonly status: Status; 101 | } 102 | 103 | export interface RepositoryState { 104 | readonly HEAD: Branch | undefined; 105 | readonly refs: Ref[]; 106 | readonly remotes: Remote[]; 107 | readonly submodules: Submodule[]; 108 | readonly rebaseCommit: Commit | undefined; 109 | 110 | readonly mergeChanges: Change[]; 111 | readonly indexChanges: Change[]; 112 | readonly workingTreeChanges: Change[]; 113 | 114 | readonly onDidChange: Event; 115 | } 116 | 117 | export interface RepositoryUIState { 118 | readonly selected: boolean; 119 | readonly onDidChange: Event; 120 | } 121 | 122 | /** 123 | * Log options. 124 | */ 125 | export interface LogOptions { 126 | /** Max number of log entries to retrieve. If not specified, the default is 32. */ 127 | readonly maxEntries?: number; 128 | readonly path?: string; 129 | } 130 | 131 | export interface CommitOptions { 132 | all?: boolean | "tracked"; 133 | amend?: boolean; 134 | signoff?: boolean; 135 | signCommit?: boolean; 136 | empty?: boolean; 137 | noVerify?: boolean; 138 | requireUserConfig?: boolean; 139 | } 140 | 141 | export interface FetchOptions { 142 | remote?: string; 143 | ref?: string; 144 | all?: boolean; 145 | prune?: boolean; 146 | depth?: number; 147 | } 148 | 149 | export interface BranchQuery { 150 | readonly remote?: boolean; 151 | readonly pattern?: string; 152 | readonly count?: number; 153 | readonly contains?: string; 154 | } 155 | 156 | export interface Repository { 157 | readonly rootUri: Uri; 158 | readonly inputBox: InputBox; 159 | readonly state: RepositoryState; 160 | readonly ui: RepositoryUIState; 161 | 162 | getConfigs(): Promise<{ key: string; value: string }[]>; 163 | getConfig(key: string): Promise; 164 | setConfig(key: string, value: string): Promise; 165 | getGlobalConfig(key: string): Promise; 166 | 167 | getObjectDetails( 168 | treeish: string, 169 | path: string 170 | ): Promise<{ mode: string; object: string; size: number }>; 171 | detectObjectType( 172 | object: string 173 | ): Promise<{ mimetype: string; encoding?: string }>; 174 | buffer(ref: string, path: string): Promise; 175 | show(ref: string, path: string): Promise; 176 | getCommit(ref: string): Promise; 177 | 178 | add(paths: string[]): Promise; 179 | clean(paths: string[]): Promise; 180 | 181 | apply(patch: string, reverse?: boolean): Promise; 182 | diff(cached?: boolean): Promise; 183 | diffWithHEAD(): Promise; 184 | diffWithHEAD(path: string): Promise; 185 | diffWith(ref: string): Promise; 186 | diffWith(ref: string, path: string): Promise; 187 | diffIndexWithHEAD(): Promise; 188 | diffIndexWithHEAD(path: string): Promise; 189 | diffIndexWith(ref: string): Promise; 190 | diffIndexWith(ref: string, path: string): Promise; 191 | diffBlobs(object1: string, object2: string): Promise; 192 | diffBetween(ref1: string, ref2: string): Promise; 193 | diffBetween(ref1: string, ref2: string, path: string): Promise; 194 | 195 | hashObject(data: string): Promise; 196 | 197 | createBranch(name: string, checkout: boolean, ref?: string): Promise; 198 | deleteBranch(name: string, force?: boolean): Promise; 199 | getBranch(name: string): Promise; 200 | getBranches(query: BranchQuery): Promise; 201 | setBranchUpstream(name: string, upstream: string): Promise; 202 | 203 | getMergeBase(ref1: string, ref2: string): Promise; 204 | 205 | tag(name: string, upstream: string): Promise; 206 | deleteTag(name: string): Promise; 207 | 208 | status(): Promise; 209 | checkout(treeish: string): Promise; 210 | 211 | addRemote(name: string, url: string): Promise; 212 | removeRemote(name: string): Promise; 213 | renameRemote(name: string, newName: string): Promise; 214 | 215 | fetch(options?: FetchOptions): Promise; 216 | fetch(remote?: string, ref?: string, depth?: number): Promise; 217 | pull(unshallow?: boolean): Promise; 218 | push( 219 | remoteName?: string, 220 | branchName?: string, 221 | setUpstream?: boolean, 222 | force?: ForcePushMode 223 | ): Promise; 224 | 225 | blame(path: string): Promise; 226 | log(options?: LogOptions): Promise; 227 | 228 | commit(message: string, opts?: CommitOptions): Promise; 229 | } 230 | 231 | export interface RemoteSource { 232 | readonly name: string; 233 | readonly description?: string; 234 | readonly url: string | string[]; 235 | } 236 | 237 | export interface RemoteSourceProvider { 238 | readonly name: string; 239 | readonly icon?: string; // codicon name 240 | readonly supportsQuery?: boolean; 241 | getRemoteSources(query?: string): ProviderResult; 242 | getBranches?(url: string): ProviderResult; 243 | publishRepository?(repository: Repository): Promise; 244 | } 245 | 246 | export interface RemoteSourcePublisher { 247 | readonly name: string; 248 | readonly icon?: string; // codicon name 249 | publishRepository(repository: Repository): Promise; 250 | } 251 | 252 | export interface Credentials { 253 | readonly username: string; 254 | readonly password: string; 255 | } 256 | 257 | export interface CredentialsProvider { 258 | getCredentials(host: Uri): ProviderResult; 259 | } 260 | 261 | export interface PushErrorHandler { 262 | handlePushError( 263 | repository: Repository, 264 | remote: Remote, 265 | refspec: string, 266 | error: Error & { gitErrorCode: GitErrorCodes } 267 | ): Promise; 268 | } 269 | 270 | export type APIState = "uninitialized" | "initialized"; 271 | 272 | export interface PublishEvent { 273 | repository: Repository; 274 | branch?: string; 275 | } 276 | 277 | export interface API { 278 | readonly state: APIState; 279 | readonly onDidChangeState: Event; 280 | readonly onDidPublish: Event; 281 | readonly git: Git; 282 | readonly repositories: Repository[]; 283 | readonly onDidOpenRepository: Event; 284 | readonly onDidCloseRepository: Event; 285 | 286 | toGitUri(uri: Uri, ref: string): Uri; 287 | getRepository(uri: Uri): Repository | null; 288 | init(root: Uri): Promise; 289 | openRepository(root: Uri): Promise; 290 | 291 | registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable; 292 | registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; 293 | registerCredentialsProvider(provider: CredentialsProvider): Disposable; 294 | registerPushErrorHandler(handler: PushErrorHandler): Disposable; 295 | } 296 | 297 | export interface GitExtension { 298 | readonly enabled: boolean; 299 | readonly onDidChangeEnablement: Event; 300 | 301 | /** 302 | * Returns a specific API version. 303 | * 304 | * Throws error if git extension is disabled. You can listed to the 305 | * [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event 306 | * to know when the extension becomes enabled/disabled. 307 | * 308 | * @param version Version number. 309 | * @returns API instance 310 | */ 311 | getAPI(version: 1): API; 312 | } 313 | 314 | export const enum GitErrorCodes { 315 | BadConfigFile = "BadConfigFile", 316 | AuthenticationFailed = "AuthenticationFailed", 317 | NoUserNameConfigured = "NoUserNameConfigured", 318 | NoUserEmailConfigured = "NoUserEmailConfigured", 319 | NoRemoteRepositorySpecified = "NoRemoteRepositorySpecified", 320 | NotAGitRepository = "NotAGitRepository", 321 | NotAtRepositoryRoot = "NotAtRepositoryRoot", 322 | Conflict = "Conflict", 323 | StashConflict = "StashConflict", 324 | UnmergedChanges = "UnmergedChanges", 325 | PushRejected = "PushRejected", 326 | RemoteConnectionError = "RemoteConnectionError", 327 | DirtyWorkTree = "DirtyWorkTree", 328 | CantOpenResource = "CantOpenResource", 329 | GitNotFound = "GitNotFound", 330 | CantCreatePipe = "CantCreatePipe", 331 | PermissionDenied = "PermissionDenied", 332 | CantAccessRemote = "CantAccessRemote", 333 | RepositoryNotFound = "RepositoryNotFound", 334 | RepositoryIsLocked = "RepositoryIsLocked", 335 | BranchNotFullyMerged = "BranchNotFullyMerged", 336 | NoRemoteReference = "NoRemoteReference", 337 | InvalidBranchName = "InvalidBranchName", 338 | BranchAlreadyExists = "BranchAlreadyExists", 339 | NoLocalChanges = "NoLocalChanges", 340 | NoStashFound = "NoStashFound", 341 | LocalChangesOverwritten = "LocalChangesOverwritten", 342 | NoUpstreamBranch = "NoUpstreamBranch", 343 | IsInSubmodule = "IsInSubmodule", 344 | WrongCase = "WrongCase", 345 | CantLockRef = "CantLockRef", 346 | CantRebaseMultipleBranches = "CantRebaseMultipleBranches", 347 | PatchDoesNotApply = "PatchDoesNotApply", 348 | NoPathFound = "NoPathFound", 349 | UnknownPath = "UnknownPath", 350 | } 351 | -------------------------------------------------------------------------------- /src/typings/ref.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "tunnel"; 4 | declare module "ssh-config"; 5 | 6 | declare module "tweetsodium"; 7 | declare module "atob"; 8 | declare module "btoa"; 9 | declare module "util"; 10 | -------------------------------------------------------------------------------- /src/utils/array.ts: -------------------------------------------------------------------------------- 1 | export function flatten(array: T[][]): T[] { 2 | const r: T[] = []; 3 | 4 | array.forEach(x => { 5 | x.forEach(y => { 6 | r.push(y); 7 | }); 8 | }); 9 | 10 | return r; 11 | } 12 | -------------------------------------------------------------------------------- /src/workflow/diagnostics.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { complete, hover, parse } from "github-actions-parser"; 4 | 5 | import { getGitHubContextForDocumentUri } from "../git/repository"; 6 | 7 | const WorkflowSelector = { 8 | pattern: "**/.github/workflows/*.{yaml,yml}", 9 | }; 10 | 11 | export function init(context: vscode.ExtensionContext) { 12 | // Register auto-complete 13 | vscode.languages.registerCompletionItemProvider( 14 | WorkflowSelector, 15 | new WorkflowCompletionItemProvider(), 16 | "." 17 | ); 18 | 19 | vscode.languages.registerHoverProvider( 20 | WorkflowSelector, 21 | new WorkflowHoverProvider() 22 | ); 23 | 24 | // 25 | // Provide diagnostics information 26 | // 27 | const collection = 28 | vscode.languages.createDiagnosticCollection("github-actions"); 29 | if (vscode.window.activeTextEditor) { 30 | updateDiagnostics(vscode.window.activeTextEditor.document, collection); 31 | } 32 | context.subscriptions.push( 33 | vscode.window.onDidChangeActiveTextEditor((editor) => { 34 | if (editor) { 35 | updateDiagnostics(editor.document, collection); 36 | } 37 | }) 38 | ); 39 | 40 | context.subscriptions.push( 41 | vscode.workspace.onDidChangeTextDocument((e) => 42 | updateDiagnostics(e.document, collection) 43 | ) 44 | ); 45 | 46 | context.subscriptions.push( 47 | vscode.workspace.onDidCloseTextDocument((doc) => collection.delete(doc.uri)) 48 | ); 49 | } 50 | 51 | async function updateDiagnostics( 52 | document: vscode.TextDocument, 53 | collection: vscode.DiagnosticCollection 54 | ): Promise { 55 | if ( 56 | document && 57 | document.fileName.match("(.*)?.github/workflows/(.*).ya?ml") 58 | ) { 59 | collection.clear(); 60 | 61 | const gitHubRepoContext = await getGitHubContextForDocumentUri( 62 | document.uri 63 | ); 64 | if (!gitHubRepoContext) { 65 | return; 66 | } 67 | 68 | const result = await parse( 69 | { 70 | ...gitHubRepoContext, 71 | repository: gitHubRepoContext.name, 72 | }, 73 | document.uri.fsPath, 74 | document.getText() 75 | ); 76 | if (result.diagnostics.length > 0) { 77 | collection.set( 78 | document.uri, 79 | result.diagnostics.map((x) => ({ 80 | severity: vscode.DiagnosticSeverity.Error, 81 | message: x.message, 82 | range: new vscode.Range( 83 | document.positionAt(x.pos[0]), 84 | document.positionAt(x.pos[1]) 85 | ), 86 | })) 87 | ); 88 | } 89 | } else { 90 | collection.clear(); 91 | } 92 | } 93 | 94 | export class WorkflowHoverProvider implements vscode.HoverProvider { 95 | async provideHover( 96 | document: vscode.TextDocument, 97 | position: vscode.Position, 98 | token: vscode.CancellationToken 99 | ): Promise { 100 | try { 101 | const gitHubRepoContext = await getGitHubContextForDocumentUri( 102 | document.uri 103 | ); 104 | if (!gitHubRepoContext) { 105 | return null; 106 | } 107 | 108 | const hoverResult = await hover( 109 | { 110 | ...gitHubRepoContext, 111 | repository: gitHubRepoContext.name, 112 | }, 113 | document.uri.fsPath, 114 | document.getText(), 115 | document.offsetAt(position) 116 | ); 117 | 118 | if (hoverResult?.description) { 119 | return { 120 | contents: [hoverResult?.description], 121 | }; 122 | } 123 | } catch (e) { 124 | // TODO: CS: handle 125 | debugger; 126 | } 127 | 128 | return null; 129 | } 130 | } 131 | 132 | export class WorkflowCompletionItemProvider 133 | implements vscode.CompletionItemProvider 134 | { 135 | async provideCompletionItems( 136 | document: vscode.TextDocument, 137 | position: vscode.Position, 138 | cancellationToken: vscode.CancellationToken 139 | ): Promise { 140 | try { 141 | const gitHubRepoContext = await getGitHubContextForDocumentUri( 142 | document.uri 143 | ); 144 | if (!gitHubRepoContext) { 145 | return []; 146 | } 147 | 148 | const completionResult = await complete( 149 | { 150 | ...gitHubRepoContext, 151 | repository: gitHubRepoContext.name, 152 | }, 153 | document.uri.fsPath, 154 | document.getText(), 155 | document.offsetAt(position) 156 | ); 157 | 158 | if (completionResult.length > 0) { 159 | return completionResult.map((x) => { 160 | const completionItem = new vscode.CompletionItem( 161 | x.value, 162 | vscode.CompletionItemKind.Constant 163 | ); 164 | 165 | // Fix the replacement range. By default VS Code looks for the current word, which leads to duplicate 166 | // replacements for something like `runs-|` which auto-completes to `runs-runs-on` 167 | const text = document.getText( 168 | new vscode.Range( 169 | position.line, 170 | Math.max(0, position.character - x.value.length), 171 | position.line, 172 | position.character 173 | ) 174 | ); 175 | for (let i = x.value.length; i >= 0; --i) { 176 | if (text.endsWith(x.value.substr(0, i))) { 177 | completionItem.range = new vscode.Range( 178 | position.line, 179 | Math.max(0, position.character - i), 180 | position.line, 181 | position.character 182 | ); 183 | break; 184 | } 185 | } 186 | 187 | if (x.description) { 188 | completionItem.documentation = new vscode.MarkdownString( 189 | x.description 190 | ); 191 | } 192 | 193 | return completionItem; 194 | }); 195 | } 196 | } catch (e) { 197 | // Ignore error 198 | return []; 199 | } 200 | 201 | return []; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/workflow/workflow.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { parse } from "github-actions-parser"; 4 | import { Workflow } from "github-actions-parser/dist/lib/workflow"; 5 | import { safeLoad } from "js-yaml"; 6 | import { basename } from "path"; 7 | import { GitHubRepoContext } from "../git/repository"; 8 | 9 | interface On { 10 | event: string; 11 | types?: string[]; 12 | branches?: string[]; 13 | schedule?: string[]; 14 | } 15 | 16 | function getEvents(doc: any): On[] { 17 | let trigger: string | string[] | { [trigger: string]: any | undefined } = 18 | doc.on; 19 | 20 | const on: On[] = []; 21 | 22 | if (trigger == undefined) { 23 | return []; 24 | } else if (typeof trigger == "string") { 25 | on.push({ 26 | event: trigger, 27 | }); 28 | } else if (Array.isArray(trigger)) { 29 | on.push( 30 | ...trigger.map((t) => ({ 31 | event: t, 32 | })) 33 | ); 34 | } else if (typeof trigger == "object") { 35 | on.push( 36 | ...Object.keys(trigger).map((event) => { 37 | // Work around typing :( 38 | const t = (trigger as any)[event]; 39 | 40 | return { 41 | event, 42 | types: t?.types, 43 | branches: t?.branches, 44 | schedule: t?.schedule, 45 | }; 46 | }) 47 | ); 48 | } 49 | 50 | return on; 51 | } 52 | 53 | export async function getContextStringForWorkflow( 54 | path: string 55 | ): Promise { 56 | try { 57 | const content = await vscode.workspace.fs.readFile(vscode.Uri.file(path)); 58 | const file = Buffer.from(content).toString("utf8"); 59 | const doc = safeLoad(file); 60 | if (doc) { 61 | let context = ""; 62 | 63 | const events = getEvents(doc); 64 | if (events.some((t) => t.event.toLowerCase() === "repository_dispatch")) { 65 | context += "rdispatch"; 66 | } 67 | 68 | if (events.some((t) => t.event.toLowerCase() === "workflow_dispatch")) { 69 | context += "wdispatch"; 70 | } 71 | 72 | return context; 73 | } 74 | } catch (e) { 75 | // Ignore 76 | } 77 | 78 | return ""; 79 | } 80 | 81 | /** 82 | * Try to get Uri to workflow in currently open workspace folders 83 | * 84 | * @param path Path for workflow. E.g., `.github/workflows/somebuild.yaml` 85 | */ 86 | export function getWorkflowUri( 87 | gitHubRepoContext: GitHubRepoContext, 88 | path: string 89 | ): vscode.Uri | null { 90 | return vscode.Uri.joinPath(gitHubRepoContext.workspaceUri, path); 91 | } 92 | 93 | export async function parseWorkflow( 94 | uri: vscode.Uri, 95 | gitHubRepoContext: GitHubRepoContext 96 | ): Promise { 97 | try { 98 | const b = await vscode.workspace.fs.readFile(uri); 99 | const workflowInput = Buffer.from(b).toString("utf-8"); 100 | const doc = await parse( 101 | { 102 | ...gitHubRepoContext, 103 | repository: gitHubRepoContext.name, 104 | }, 105 | basename(uri.fsPath), 106 | workflowInput 107 | ); 108 | return doc.workflow; 109 | } catch { 110 | // Ignore error here 111 | } 112 | 113 | return undefined; 114 | } 115 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "dist", 6 | "lib": ["ES2019", "DOM"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "typeRoots": [ 10 | "./node_modules/@types", 11 | "./src/typings/" 12 | ], 13 | "strict": true /* enable all strict type-checking options */ 14 | /* Additional Checks */ 15 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 16 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 17 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 18 | }, 19 | "exclude": [ 20 | "node_modules", 21 | ".vscode-test" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "curly": true, 7 | "class-name": true, 8 | "semicolon": [ 9 | true, 10 | "always" 11 | ], 12 | "triple-equals": true 13 | }, 14 | "defaultSeverity": "warning" 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | "use strict"; 4 | 5 | const path = require("path"); 6 | const webpack = require('webpack'); 7 | 8 | /**@type {import('webpack').Configuration}*/ 9 | const config = { 10 | entry: "./src/extension.ts", // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 11 | devtool: "source-map", 12 | externals: { 13 | vscode: "commonjs vscode", 14 | }, 15 | plugins: [ 16 | new webpack.ProvidePlugin({ 17 | Buffer: ['buffer', 'Buffer'], 18 | }), 19 | ], 20 | resolve: { 21 | extensions: [".ts", ".js"], 22 | alias: { 23 | "universal-user-agent$": "universal-user-agent/dist-node/index.js" 24 | }, 25 | fallback: { 26 | "buffer": require.resolve("buffer/"), 27 | "path": require.resolve("path-browserify") 28 | } 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: "ts-loader" 38 | } 39 | ] 40 | }, 41 | { 42 | test: /\.node$/, 43 | use: "node-loader" 44 | } 45 | ] 46 | } 47 | }; 48 | 49 | const nodeConfig = { 50 | ...config, 51 | target: "node", 52 | output: { 53 | path: path.resolve(__dirname, "dist"), 54 | filename: "extension-node.js", 55 | libraryTarget: "commonjs2", 56 | devtoolModuleFilenameTemplate: "../[resource-path]", 57 | }, 58 | }; 59 | 60 | const webConfig = { 61 | ...config, 62 | target: "webworker", 63 | output: { 64 | path: path.resolve(__dirname, "dist"), 65 | filename: "extension-web.js", 66 | libraryTarget: "commonjs2", 67 | devtoolModuleFilenameTemplate: "../[resource-path]", 68 | }, 69 | }; 70 | 71 | module.exports = [nodeConfig, webConfig]; --------------------------------------------------------------------------------