├── .gitignore ├── images ├── logo.png ├── doc │ ├── demo.gif │ ├── views.png │ ├── demo_setup.gif │ ├── pipeline_view.png │ ├── demo_build_query.gif │ ├── demo_pipelinerun.gif │ ├── demo_pipelinecreate.gif │ ├── demo_pipelineparams.gif │ └── connection_folderfilter_browser.png ├── pipeline-execute-dark.svg ├── pipeline-execute-light.svg ├── pipeline-open-script-dark.svg ├── build-unstable-dark.svg ├── build-unstable-light.svg ├── pipeline-open-script-light.svg ├── inactive-dark.svg ├── inactive-light.svg ├── pipe-icon-default-light.svg ├── pipe-icon-default-dark.svg ├── node-disconnected-dark.svg ├── node-enabled-dark.svg ├── node-enabled-light.svg ├── active-dark.svg ├── active-light.svg ├── build-good-dark.svg ├── build-good-light.svg ├── pipe-icon-linked-dark.svg ├── pipe-icon-linked-light.svg ├── build-aborted-dark.svg ├── build-aborted-light.svg ├── node-disconnected-light.svg ├── jenkins-bow-icon.svg ├── build-bad-dark.svg ├── build-bad-light.svg ├── node-disabled-dark.svg ├── node-disabled-light.svg ├── logo.svg ├── build-inprogress-dark.svg └── build-inprogress-light.svg ├── src ├── quickpickSet.ts ├── test │ ├── extension.test.ts │ └── index.ts ├── jobType.ts ├── extensionVariables.ts ├── stepdoc.ts ├── logger.ts ├── queueJack.ts ├── pipelineJobConfig.ts ├── jack.ts ├── jenkinsConnection.ts ├── sharedLibApiManager.ts ├── extension.ts ├── connectionsTree.ts ├── outputProvider.ts ├── queueTree.ts ├── snippets.ts ├── scriptConsoleJack.ts ├── jobTree.ts ├── selectionFlows.ts ├── nodeTree.ts ├── jobJack.ts ├── nodeJack.ts ├── utils.ts ├── buildJack.ts └── pipelineTree.ts ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── .vscodeignore ├── tslint.json ├── Jenkinsfile.ci ├── .github └── workflows │ └── main.yml ├── tsconfig.json ├── doc_update.py ├── LICENSE.md ├── syntaxes └── pipelinelog.tmGrammer.json ├── CONTRIBUTING.md ├── COMMANDS.md ├── TUTORIAL.md ├── README.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | dist 4 | *.vsix 5 | .vscode* 6 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabeyti/jenkins-jack/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/doc/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabeyti/jenkins-jack/HEAD/images/doc/demo.gif -------------------------------------------------------------------------------- /images/doc/views.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabeyti/jenkins-jack/HEAD/images/doc/views.png -------------------------------------------------------------------------------- /images/doc/demo_setup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabeyti/jenkins-jack/HEAD/images/doc/demo_setup.gif -------------------------------------------------------------------------------- /images/doc/pipeline_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabeyti/jenkins-jack/HEAD/images/doc/pipeline_view.png -------------------------------------------------------------------------------- /images/doc/demo_build_query.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabeyti/jenkins-jack/HEAD/images/doc/demo_build_query.gif -------------------------------------------------------------------------------- /images/doc/demo_pipelinerun.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabeyti/jenkins-jack/HEAD/images/doc/demo_pipelinerun.gif -------------------------------------------------------------------------------- /src/quickpickSet.ts: -------------------------------------------------------------------------------- 1 | export interface QuickpickSet { 2 | display(): Promise; 3 | commands: any[]; 4 | } -------------------------------------------------------------------------------- /images/doc/demo_pipelinecreate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabeyti/jenkins-jack/HEAD/images/doc/demo_pipelinecreate.gif -------------------------------------------------------------------------------- /images/doc/demo_pipelineparams.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabeyti/jenkins-jack/HEAD/images/doc/demo_pipelineparams.gif -------------------------------------------------------------------------------- /images/doc/connection_folderfilter_browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabeyti/jenkins-jack/HEAD/images/doc/connection_folderfilter_browser.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "ms-vscode.vscode-typescript-tslint-plugin" 6 | ] 7 | } -------------------------------------------------------------------------------- /images/pipeline-execute-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/pipeline-execute-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/tslint.json 9 | **/*.map 10 | **/*.ts 11 | *.log 12 | *.vsix 13 | *.sh 14 | *.txt 15 | *.py 16 | temp 17 | Jenkinsfile* 18 | images/doc 19 | *.orig 20 | *.html 21 | test.json 22 | notes.md -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Jenkinsfile.ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | @Library('tools')_ 4 | 5 | pipeline { 6 | agent none 7 | stages { 8 | stage ('Build') { 9 | agent any 10 | 11 | steps { 12 | sh ''' 13 | npm upgrade 14 | npm install 15 | npm run vscode:prepublish 16 | ''' 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /images/pipeline-open-script-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | 17 | - uses: actions/checkout@v2 18 | 19 | - name: vscode:prepublish 20 | run: | 21 | npm upgrade 22 | npm install 23 | npm run vscode:prepublish 24 | -------------------------------------------------------------------------------- /.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 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /images/build-unstable-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /images/build-unstable-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /images/pipeline-open-script-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /images/inactive-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /images/inactive-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /images/pipe-icon-default-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | // import * as vscode from 'vscode'; 12 | // import * as myExtension from '../extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite("Extension Tests", function () { 16 | 17 | // Defines a Mocha unit test 18 | test("Something 1", function() { 19 | assert.equal(-1, [1, 2, 3].indexOf(5)); 20 | assert.equal(-1, [1, 2, 3].indexOf(0)); 21 | }); 22 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictPropertyInitialization": false, 4 | "module": "commonjs", 5 | "target": "es6", 6 | "outDir": "out", 7 | "lib": [ 8 | "es6" 9 | ], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | /* Strict Type-Checking Option */ 13 | "strict": false, /* enable all strict type-checking options */ 14 | /* Additional Checks */ 15 | "noUnusedLocals": true /* Report errors on unused locals. */ 16 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 17 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 18 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | ".vscode-test" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /images/pipe-icon-default-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /doc_update.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | 5 | def update_settings(readme): 6 | 7 | config_md = ''' 8 | 9 | |Name |Description | 10 | | --- | ---------- | 11 | ''' 12 | 13 | with open('package.json') as f: 14 | data = json.loads(f.read()) 15 | 16 | properties = data['contributes']['configuration'][0]['properties'] 17 | for item in sorted (properties.keys()): 18 | config_md += '| `{}` | {} |\n'.format(item, properties[item]['markdownDescription']) 19 | 20 | md = readme[0:readme.index('')] 21 | md += '' 22 | md += config_md 23 | md += readme[-(len(readme) - readme.index('')):] 24 | 25 | return md 26 | 27 | 28 | with open('README.md', 'r') as f: readme = f.read() 29 | md = update_settings(readme) 30 | 31 | with open('README.md', 'w') as f: 32 | f.write(md) -------------------------------------------------------------------------------- /images/node-disconnected-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/node-enabled-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/node-enabled-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/active-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /images/active-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /images/build-good-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /images/build-good-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /images/pipe-icon-linked-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /images/pipe-icon-linked-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /src/jobType.ts: -------------------------------------------------------------------------------- 1 | export enum JobType { 2 | Default = 'default', 3 | Pipeline = 'pipeline', 4 | Multi = 'multibranch', 5 | Org = 'org', 6 | Folder = 'folder' 7 | } 8 | 9 | export class JobTypeUtil { 10 | public static classNameToType(classStr: string): JobType { 11 | switch(classStr) { 12 | case 'com.cloudbees.hudson.plugins.folder.Folder': 13 | case 'com.cloudbees.opscenter.bluesteel.folder.BlueSteelTeamFolder': { 14 | return JobType.Folder; 15 | } 16 | case 'org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject': { 17 | return JobType.Multi; 18 | break; 19 | } 20 | case 'jenkins.branch.OrganizationFolder': { 21 | return JobType.Org; 22 | } 23 | case 'org.jenkinsci.plugins.workflow.job.WorkflowJob': { 24 | return JobType.Pipeline; 25 | } 26 | default: { 27 | return JobType.Default; 28 | } 29 | } 30 | }; 31 | } -------------------------------------------------------------------------------- /images/build-aborted-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /images/build-aborted-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | import * as testRunner from 'vscode/lib/testrunner'; 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [{ 8 | "name": "Run Extension", 9 | "type": "extensionHost", 10 | "request": "launch", 11 | "runtimeExecutable": "${execPath}", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "npm: watch" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "runtimeExecutable": "${execPath}", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceFolder}", 27 | "--extensionTestsPath=${workspaceFolder}/out/test" 28 | ], 29 | "outFiles": [ 30 | "${workspaceFolder}/out/test/**/*.js" 31 | ], 32 | "preLaunchTask": "npm: watch" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /images/node-disconnected-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Jenkins Jack is licensed under the MIT License. 2 | 3 | > MIT License 4 | > 5 | > Copyright (c) 2019 Travis Abeyti 6 | > 7 | > Permission is hereby granted, free of charge, to any person obtaining a copy 8 | > of this software and associated documentation files (the "Software"), to deal 9 | > in the Software without restriction, including without limitation the rights 10 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | > copies of the Software, and to permit persons to whom the Software is 12 | > furnished to do so, subject to the following conditions: 13 | > 14 | > The above copyright notice and this permission notice shall be included in all 15 | > copies or substantial portions of the Software. 16 | > 17 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | > SOFTWARE. -------------------------------------------------------------------------------- /syntaxes/pipelinelog.tmGrammer.json: -------------------------------------------------------------------------------- 1 | { 2 | "scopeName": "source.pipelinelog", 3 | "name": "Pipeline Log", 4 | "patterns": [ 5 | { "include": "#pipeline-comment" }, 6 | { "include": "#link" }, 7 | { "include": "#shell-command" }, 8 | { "include": "#groovy-exception" } 9 | ], 10 | "repository": { 11 | "groovy-exception": { 12 | "patterns": [{ 13 | "name": "keyword.control.import.pipelinelog", 14 | "match": "(^\\d+\\) .+)|(^.+Exception: .+)|(^\\s+at .+)|(^\\s+... \\d+ more)|(^\\s*Caused by:.+)" 15 | }] 16 | }, 17 | "shell-command": { 18 | "begin": "^\\s*[\\+>]+\\s+(\\w+\\.?\\w*)\\s+(.*)", 19 | "end": "\n", 20 | "captures": { 21 | "1": { 22 | "name": "entity.name.function.pipelinelog" 23 | }, 24 | "2": { 25 | "name": "constant.language.pipelinelog" 26 | } 27 | } 28 | }, 29 | "link": { 30 | "patterns": [{ 31 | "name": "entity.name.function.pipelinelog", 32 | "match": "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)" 33 | }] 34 | }, 35 | "pipeline-comment": { 36 | "patterns": [ 37 | { 38 | "name": "comment.line.double-slash.pipelinelog", 39 | "match": "\\[Pipeline\\].*" 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /images/jenkins-bow-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 11 | 12 | 14 | 16 | 17 | 18 | 20 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/extensionVariables.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext } from "vscode"; 2 | import { ConnectionsManager } from "./connectionsManager"; 3 | import { PipelineSnippets } from "./snippets"; 4 | import { PipelineJack } from "./pipelineJack"; 5 | import { ScriptConsoleJack } from "./scriptConsoleJack"; 6 | import { NodeJack } from "./nodeJack"; 7 | import { BuildJack } from "./buildJack"; 8 | import { JobJack } from "./jobJack"; 9 | import { JobTree } from "./jobTree"; 10 | import { PipelineTree } from "./pipelineTree"; 11 | import { NodeTree } from "./nodeTree"; 12 | import { OutputPanelProvider } from "./outputProvider"; 13 | import { ConnectionsTree } from "./connectionsTree"; 14 | import { Logger } from "./logger"; 15 | import { QueueJack } from "./queueJack"; 16 | import { QueueTree } from "./queueTree"; 17 | 18 | /** 19 | * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts 20 | */ 21 | export namespace ext { 22 | export let context: ExtensionContext; 23 | export let pipelineSnippets: PipelineSnippets; 24 | 25 | export let outputPanelProvider: OutputPanelProvider; 26 | 27 | export let pipelineJack: PipelineJack; 28 | export let scriptConsoleJack: ScriptConsoleJack; 29 | export let nodeJack: NodeJack; 30 | export let buildJack: BuildJack; 31 | export let jobJack: JobJack; 32 | export let queueJack: QueueJack; 33 | 34 | export let connectionsTree: ConnectionsTree; 35 | export let jobTree: JobTree; 36 | export let pipelineTree: PipelineTree; 37 | export let nodeTree: NodeTree; 38 | export let queueTree: QueueTree; 39 | export let connectionsManager: ConnectionsManager; 40 | export let logger: Logger; 41 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Submitting a Change 2 | Fork the repo and file a PR. In the description of your PR, link the Github Issue this change relates to. 3 | 4 | ## Creating an Issue 5 | 6 | Before you create a new issue, please do a search in [open issues](https://github.com/tabeyti/jenkins-jack/issues) to see if the issue or feature request has already been filed. 7 | 8 | Be sure to scan through the [most popular](https://github.com/tabeyti/jenkins-jack/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc) feature requests. 9 | 10 | If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment: 11 | 12 | * 👍 - upvote 13 | * 👎 - downvote 14 | 15 | If you cannot find an existing issue that describes your bug or feature, create a new issue using the guidelines below. 16 | 17 | ### Writing a Good Bug Report and Feature Request 18 | 19 | File a single issue per problem and feature request. Do not enumerate multiple bugs or feature requests in the same issue. 20 | 21 | Do not add your issue as a comment to an existing issue unless it's for the identical input. Many issues look similar, but have different causes. 22 | 23 | The more information you can provide, the more likely I'll be successful at reproducing the issue and finding a fix. 24 | 25 | Please include the following with each issue: 26 | 27 | * Your operating system 28 | 29 | * Version of VS Code 30 | 31 | * Version of Jenkins Jack 32 | 33 | * Version of Jenkins that you're connecting to 34 | 35 | * Reproducible steps (1... 2... 3...) that cause the issue 36 | 37 | * What you expected to see, versus what you actually saw 38 | 39 | * Any errors or relevant output from: 40 | 41 | * The Dev Tools Console (open from the menu: Help > Toggle Developer Tools) 42 | 43 | * The **Jenkins Jack Log** output channel (from the OUTPUT panel, select **Jenkins Jack Log** from the panel drop-down) 44 | 45 | -------------------------------------------------------------------------------- /src/stepdoc.ts: -------------------------------------------------------------------------------- 1 | export class PipelineStepDoc { 2 | name: string; 3 | doc: string; 4 | params: Map; 5 | 6 | /** 7 | * Constructor. 8 | * @param name The name of the step. 9 | * @param doc The documentation for the step. 10 | * @param params A map of key/value pairs representing 11 | * the name of the parameter and it's type, repsectively. 12 | */ 13 | constructor(name: string, doc: string, params: Map) { 14 | this.name = name; 15 | this.doc = doc; 16 | this.params = params; 17 | } 18 | 19 | /** 20 | * Returns the snippet method skeleton string for this step. 21 | */ 22 | public getSnippet() { 23 | let p = new Array(); 24 | this.params.forEach((value: string, key: string) => { 25 | value = this.paramDefaultValue(value); 26 | p.push(`${key}: ${value}`); 27 | }); 28 | return `${this.name} ${p.join(', ')}`; 29 | } 30 | 31 | /** 32 | * Returns the method signature string for this step. 33 | * Used in snippets 'describe'. 34 | */ 35 | public getSignature() { 36 | let p = Array(); 37 | this.params.forEach((value: string, key: string) => { 38 | p.push(`${key}: ${value}`); 39 | }); 40 | return `${this.name}(${p.join(', ')})`; 41 | } 42 | 43 | private paramDefaultValue(param: string) { 44 | param = param.replace("'", ""); 45 | switch(param) { 46 | case "java.lang.String": 47 | return "\"\""; 48 | case "Closure": 49 | return "\{\}"; 50 | case "Map": 51 | return "[:]"; 52 | case "java.lang.Integer": 53 | case "int": 54 | return "0"; 55 | case "boolean": 56 | return "true"; 57 | case "java.lang.Object": 58 | return "null"; 59 | default: 60 | return "[unknown_param]"; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as dayjs from 'dayjs'; 3 | 4 | export class Logger { 5 | private _outputChannel: vscode.OutputChannel; 6 | private _level: Level; 7 | 8 | private readonly levelStringMap = [ 9 | 'trace', 10 | 'debug', 11 | 'info', 12 | 'warning', 13 | 'error' 14 | ] 15 | 16 | protected readonly barrierLine: string = '-'.repeat(80); 17 | 18 | constructor() { 19 | this._outputChannel = vscode.window.createOutputChannel("Jenkins Jack Log"); 20 | 21 | // TODO: make this config driven 22 | this._level = Level.Info; 23 | } 24 | 25 | public trace(message: any) { 26 | if (this._level > Level.Trace) { return; } 27 | this.out(Level.Trace, message); 28 | } 29 | 30 | public debug(message: any) { 31 | if (this._level > Level.Debug) { return; } 32 | this.out(Level.Debug, message); 33 | } 34 | 35 | public info(message: any) { 36 | if (this._level > Level.Info) { return; } 37 | this.out(Level.Info, message); 38 | } 39 | 40 | public warn(message: any) { 41 | if (this._level > Level.Warning) { return; } 42 | this.out(Level.Warning, message); 43 | } 44 | 45 | public error(message: any) { 46 | if (this._level > Level.Error) { return; } 47 | this.out(Level.Error, message); 48 | } 49 | 50 | private out(level: Level, message: any) { 51 | 52 | let caller = 'jenkins-jack' 53 | try { throw new Error(); } 54 | catch (e) { 55 | let ex = e as any; 56 | // HACK: parses the call-stack for a specific line to grab the calling module 57 | caller = ex.stack.split('\n')[3].match(/.*[\/|\\](.*)\.js.*/)[1]; 58 | } 59 | let logLine = `[${dayjs().format('DD-MM-YYYY HH:mm:ss')}] [${caller}] [${this.levelStringMap[level]}] - ${message}`; 60 | 61 | this._outputChannel.appendLine(logLine); 62 | console.log(logLine); 63 | } 64 | } 65 | 66 | export enum Level { 67 | Trace = 0, 68 | Debug, 69 | Info, 70 | Warning, 71 | Error, 72 | Failure 73 | } 74 | -------------------------------------------------------------------------------- /images/build-bad-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 14 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /images/build-bad-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 14 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/queueJack.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { JackBase } from './jack'; 3 | import { ext } from './extensionVariables'; 4 | import { withProgressOutputParallel } from './utils'; 5 | import { QueueTreeItem } from './queueTree'; 6 | 7 | export class QueueJack extends JackBase { 8 | 9 | constructor() { 10 | super('Queue Jack', 'extension.jenkins-jack.queue'); 11 | 12 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.queue.cancel', async (item?: any[] | QueueTreeItem, items?: QueueTreeItem[]) => { 13 | if (item instanceof QueueTreeItem) { 14 | items = !items ? [item.queueItem] : items.map((item: any) => item.queueItem); 15 | } else { 16 | items = await ext.connectionsManager.host.getQueueItems(); 17 | if (undefined == items) { return; } 18 | items = await vscode.window.showQuickPick(items, { 19 | canPickMany: true, 20 | ignoreFocusOut: true, 21 | placeHolder: 'Queue items to cancel.', 22 | matchOnDetail: true, 23 | matchOnDescription: true 24 | }) as any; 25 | if (undefined == items) { return; } 26 | } 27 | 28 | let queueItemNames = items.map((i: any) => `${i.name} - ${i.why}`); 29 | let r = await this.showInformationModal( 30 | `Are you sure you want to cancel these queue items?\n\n${queueItemNames.join('\n')}`, 31 | { title: "Yes"} ); 32 | if (undefined === r) { return undefined; } 33 | 34 | let output = await withProgressOutputParallel('Queue Jack Output(s)', items, async (item) => { 35 | await ext.connectionsManager.host.queueCancel(item.id); 36 | return `Cancelled queue item: ${item.id} - ${item.why}`; 37 | }); 38 | this.outputChannel.clear(); 39 | this.outputChannel.show(); 40 | this.outputChannel.appendLine(output); 41 | 42 | ext.jobTree.refresh(); 43 | ext.nodeTree.refresh(2); 44 | ext.queueTree.refresh(); 45 | })); 46 | } 47 | 48 | public get commands(): any[] { 49 | return [ 50 | { 51 | label: "$(stop) Queue: Cancel", 52 | description: "Cancels an item in queue.", 53 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.queue.cancel') 54 | } 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /images/node-disabled-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/node-disabled-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/pipelineJobConfig.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import { readjson, writejson } from './utils'; 5 | 6 | export class PipelineConfig { 7 | public name: string; 8 | public params: any; 9 | public interactiveInputOverride: any; 10 | public path: string; 11 | 12 | constructor(public readonly scriptPath: string, public folder?: string, overwrite: boolean = false) { 13 | let parsed = path.parse(scriptPath); 14 | this.path = PipelineConfig.pathFromScript(scriptPath); 15 | 16 | // If config doesn't exist, write out defaults. 17 | if (!fs.existsSync(this.path) || overwrite) { 18 | this.name = parsed.name; 19 | this.params = null; 20 | this.folder = folder; 21 | this.save(); 22 | return; 23 | } 24 | 25 | let json = readjson(this.path); 26 | this.name = json.name; 27 | this.params = json.params; 28 | this.interactiveInputOverride = json.interactiveInputOverride; 29 | this.folder = json.folder; 30 | } 31 | 32 | toJSON(): any { 33 | return { 34 | name: this.name, 35 | params: this.params, 36 | interactiveInputOverride: this.interactiveInputOverride, 37 | folder: this.folder 38 | }; 39 | } 40 | 41 | fromJSON(json: any): PipelineConfig { 42 | let pc = Object.create(PipelineConfig.prototype); 43 | return Object.assign(pc, json, { 44 | name: json.name, 45 | params: json.params, 46 | interactiveInputOverride: json.interactiveInputOverride, 47 | folder: json.folder 48 | }); 49 | } 50 | 51 | get buildableName(): string { 52 | if (undefined === this.folder || '' === this.folder) { 53 | return this.name; 54 | } 55 | return `${this.folder}/${this.name}`; 56 | } 57 | 58 | 59 | /** 60 | * Saves the current pipeline configuration to disk. 61 | */ 62 | public save() { 63 | writejson(this.path, this); 64 | } 65 | 66 | /** 67 | * Updates the class properties with the saved 68 | * configuration values. 69 | */ 70 | public update() { 71 | let json = readjson(this.path); 72 | this.name = json.name; 73 | this.params = json.params; 74 | this.interactiveInputOverride = json.interactiveInputOverride; 75 | this.folder = json.folder; 76 | } 77 | 78 | public static exists(scriptPath: string): boolean { 79 | let path = PipelineConfig.pathFromScript(scriptPath); 80 | return fs.existsSync(path); 81 | } 82 | 83 | public static pathFromScript(scriptPath: string): string { 84 | let parsed = path.parse(scriptPath); 85 | let configFileName = `.${parsed.name}.config.json`; 86 | return path.join(parsed.dir, configFileName); 87 | } 88 | } -------------------------------------------------------------------------------- /src/jack.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { QuickpickSet } from './quickpickSet'; 3 | import { ext } from './extensionVariables'; 4 | 5 | export abstract class JackBase implements QuickpickSet { 6 | [key: string]: any; 7 | outputChannel: vscode.OutputChannel; 8 | readonly name: string; 9 | protected readonly barrierLine: string = '-'.repeat(80); 10 | 11 | private outputViewType: string; 12 | 13 | constructor(name: string, command: string) { 14 | this.name = name; 15 | let disposable = vscode.commands.registerCommand(command, async () => { 16 | try { 17 | await this.display(); 18 | } catch (err) { 19 | vscode.window.showWarningMessage(`Could not display ${command} commands.`); 20 | } 21 | }); 22 | ext.context.subscriptions.push(disposable); 23 | 24 | let config = vscode.workspace.getConfiguration('jenkins-jack.outputView'); 25 | this.outputViewType = config.type; 26 | this.updateOutputChannel(this.outputViewType); 27 | 28 | vscode.workspace.onDidChangeConfiguration((event: vscode.ConfigurationChangeEvent) => { 29 | if (event.affectsConfiguration('jenkins-jack.outputView')) { 30 | let config = vscode.workspace.getConfiguration('jenkins-jack.outputView'); 31 | if (config.type !== this.outputViewType) { 32 | this.outputViewType = config.type; 33 | this.updateOutputChannel(this.outputViewType); 34 | } 35 | } 36 | }); 37 | } 38 | 39 | private updateOutputChannel(type: string) { 40 | if ("channel" === type) { 41 | this.outputChannel = vscode.window.createOutputChannel(this.name); 42 | } 43 | else if ("panel" === type) { 44 | this.outputChannel = ext.outputPanelProvider.get(`${this.name} Output`); 45 | } 46 | else { 47 | throw new Error("Invalid 'view' type for output."); 48 | } 49 | } 50 | 51 | public abstract get commands(): any[]; 52 | 53 | public async display(): Promise { 54 | let commands = this.commands; 55 | if (0 === commands.length) { return; } 56 | 57 | let result = await vscode.window.showQuickPick(commands, { placeHolder: 'Jenkins Jack' }); 58 | if (undefined === result) { return; } 59 | 60 | return result.target(); 61 | } 62 | 63 | public async showInformationMessage(message: string, ...items: T[]): Promise { 64 | return vscode.window.showInformationMessage(`${this.name}: ${message}`, ...items); 65 | } 66 | 67 | public async showInformationModal(message: string, ...items: T[]): Promise { 68 | return vscode.window.showInformationMessage(`${this.name}: ${message}`, { modal: true }, ...items); 69 | } 70 | 71 | public async showWarningMessage(message: string, ...items: T[]): Promise { 72 | return vscode.window.showWarningMessage(`${this.name}: ${message}`, ...items); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/jenkinsConnection.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as keytar from 'keytar'; 3 | 4 | export class JenkinsConnection { 5 | public constructor( 6 | public readonly name: string, 7 | public readonly uri: string, 8 | public readonly username: string, 9 | public readonly crumbIssuer: boolean, 10 | public readonly active: boolean, 11 | public folderFilter?: string) { 12 | if ('' == this.username) { 13 | this.username = 'default'; 14 | } 15 | } 16 | 17 | public get serviceName(): string { return `jenkins-jack:${this.name}`; } 18 | 19 | /** 20 | * Retrieves the password for the connection. 21 | * @param ignoreMissingPassword Optional flag to ignore the prompting of entering in missing password. 22 | * @returns The password as a string, otherwise undefined. 23 | */ 24 | public async getPassword(ignoreMissingPassword?: boolean): Promise { 25 | let password: string | null | undefined = await keytar.getPassword(this.serviceName, this.username); 26 | if (null != password) { return password; } 27 | 28 | if (ignoreMissingPassword) { return; undefined; } 29 | 30 | let message = `Could not retrieve password from local key-chain for: ${this.serviceName} - ${this.username}.\n\nWould you like to add it?`; 31 | let result = await vscode.window.showInformationMessage(message, { modal: true }, { title: 'Yes' } ); 32 | if (undefined === result) { return undefined; } 33 | 34 | password = await vscode.window.showInputBox({ 35 | ignoreFocusOut: true, 36 | password: true, 37 | prompt: `Enter in the password for "${this.username}" for authentication. Passwords are stored on the local system's key-chain. `, 38 | }); 39 | if (undefined === password) { return undefined; } 40 | await this.setPassword(password); 41 | return password; 42 | } 43 | 44 | /** 45 | * Sets the password for the connection. 46 | * @param password The password to store. 47 | */ 48 | public async setPassword(password: string): Promise { 49 | return await keytar.setPassword(this.serviceName, this .username, password); 50 | } 51 | 52 | /** 53 | * Deletes the password for the connection. 54 | */ 55 | public async deletePassword(): Promise { 56 | return await keytar.deletePassword(this.serviceName, this.username); 57 | } 58 | 59 | public static fromJSON(json: any) : JenkinsConnection { 60 | let thing = new JenkinsConnection( 61 | json.name, 62 | json.uri, 63 | json.username ?? 'default', 64 | (null != json.crumbIssuer) ? json.crumbIssuer : true, 65 | json.active ?? false, 66 | json.folderFilter 67 | ); 68 | return thing; 69 | } 70 | 71 | public toJSON(): any { 72 | return { 73 | name: this.name, 74 | uri: this.uri, 75 | username: this.username, 76 | folderFilter: this.folderFilter, 77 | crumbIssuer: this.crumbIssuer, 78 | active: this.active 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/sharedLibApiManager.ts: -------------------------------------------------------------------------------- 1 | import * as htmlParser from 'cheerio'; 2 | import { ext } from './extensionVariables'; 3 | import { folderToUri } from './utils'; 4 | 5 | export class SharedLibVar { 6 | label: string; 7 | description?: string; 8 | descriptionHtml?: string; 9 | 10 | /** 11 | * Constructor. 12 | * @param name The name of the definition. 13 | * @param description The description. 14 | * @param descriptionHtml The html of the description. 15 | */ 16 | constructor(name: string, description: string, descriptionHtml: string) { 17 | this.label = name; 18 | this.description = description; 19 | this.descriptionHtml = descriptionHtml; 20 | } 21 | } 22 | 23 | /** 24 | * Hello everyone! I'm another singleton. 25 | * Would you like another poor design choice to 26 | * go with your laziness? 27 | */ 28 | export class SharedLibApiManager { 29 | public sharedLibVars: SharedLibVar[]; 30 | 31 | private static sharedLibInstance: SharedLibApiManager; 32 | 33 | private constructor() { 34 | this.sharedLibVars = []; 35 | } 36 | 37 | public static get instance() { 38 | if (undefined === SharedLibApiManager.sharedLibInstance) { 39 | SharedLibApiManager.sharedLibInstance = new SharedLibApiManager(); 40 | } 41 | return SharedLibApiManager.sharedLibInstance; 42 | } 43 | 44 | /** 45 | * Retrieves/parses Shared Library/Global Variable definitions. 46 | * @param job Optional Jenkins API job (json blob) to retrieve global-vars/shared-library from. 47 | * E.g. /pipeline-syntax/globals vs. /job/somejob/pipeline-syntax/globals 48 | */ 49 | public async refresh(job: any | undefined = undefined) { 50 | let url = undefined !== job ? `job/${folderToUri(job.fullName)}/pipeline-syntax/globals` : 51 | 'pipeline-syntax/globals'; 52 | 53 | let html: string = await ext.connectionsManager.host.get(url); 54 | if (undefined === html) { return; } 55 | 56 | this.sharedLibVars = this.parseHtml(html); 57 | return this.sharedLibVars; 58 | } 59 | 60 | /** 61 | * Parses the html of the Global Variables/Shared Library page for 62 | * definitions. 63 | * @param html The Shared Library/Global Variables html as a string. 64 | */ 65 | private parseHtml(html: string) { 66 | const root = htmlParser.load(html); 67 | let doc = root('.steps.variables.root').first(); 68 | 69 | let sharedLibVars: any[] = []; 70 | let child = doc.find('dt').first(); 71 | while (0 < child.length) { 72 | // Grab name, description, and html for the shared var. 73 | let name = child.attr('id'); 74 | let descr = child.next('dd').find('div').first().text().trim(); 75 | let html = child.next('dd').find('div').first().html(); 76 | if (null === descr || null === html) { continue; } 77 | 78 | if (undefined === name) { name = 'undefined'; } 79 | 80 | // Add shared var name as title to the content. 81 | html = `

${name}

${html}
`; 82 | if (!sharedLibVars.some((slv: SharedLibVar) => slv.label === name)) { 83 | sharedLibVars.push(new SharedLibVar(name, descr, html)); 84 | } 85 | 86 | // Get the next shared var. 87 | child = child.next('dd').next('dt'); 88 | } 89 | return sharedLibVars; 90 | } 91 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { PipelineJack } from './pipelineJack'; 3 | import { PipelineSnippets } from './snippets'; 4 | import { ScriptConsoleJack } from './scriptConsoleJack'; 5 | import { BuildJack } from './buildJack'; 6 | import { ConnectionsManager } from './connectionsManager'; 7 | import { NodeJack } from './nodeJack'; 8 | import { JobJack } from './jobJack'; 9 | import { OutputPanelProvider } from './outputProvider'; 10 | import { QuickpickSet } from './quickpickSet'; 11 | import { PipelineTree } from './pipelineTree'; 12 | import { JobTree } from './jobTree'; 13 | import { NodeTree } from './nodeTree'; 14 | import { ext } from './extensionVariables'; 15 | import { applyBackwardsCompat } from './utils'; 16 | import { ConnectionsTree } from './connectionsTree'; 17 | import { Logger } from './logger'; 18 | import { QueueJack } from './queueJack'; 19 | import { QueueTree } from './queueTree'; 20 | 21 | export async function activate(context: vscode.ExtensionContext) { 22 | 23 | await applyBackwardsCompat(); 24 | 25 | ext.context = context; 26 | 27 | ext.logger = new Logger(); 28 | 29 | // We initialize the Jenkins service first in order to avoid 30 | // a race condition during onDidChangeConfiguration 31 | let commandSets: QuickpickSet[] = []; 32 | ext.connectionsManager = new ConnectionsManager(); 33 | await ext.connectionsManager.initialize(); 34 | 35 | ext.pipelineSnippets = new PipelineSnippets(); 36 | 37 | // Initialize the output panel provider for jack command output 38 | ext.outputPanelProvider = new OutputPanelProvider(); 39 | 40 | // Initialize top level jacks and gather their sub-commands for the Jack command 41 | // quick-pick display 42 | ext.pipelineJack = new PipelineJack(); 43 | commandSets.push(ext.pipelineJack); 44 | 45 | ext.scriptConsoleJack = new ScriptConsoleJack(); 46 | commandSets.push(ext.scriptConsoleJack); 47 | 48 | ext.jobJack = new JobJack(); 49 | commandSets.push(ext.jobJack); 50 | 51 | ext.buildJack = new BuildJack(); 52 | commandSets.push(ext.buildJack); 53 | 54 | ext.nodeJack = new NodeJack(); 55 | commandSets.push(ext.nodeJack); 56 | 57 | ext.queueJack = new QueueJack(); 58 | commandSets.push(ext.queueJack); 59 | 60 | commandSets.push(ext.connectionsManager); 61 | 62 | ext.logger.info('Extension Jenkins Jack now active!'); 63 | 64 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.jacks', async () => { 65 | 66 | // Build up quick pick list 67 | let selections: any[] = []; 68 | for (let c of commandSets) { 69 | let cmds = c.commands; 70 | if (0 === cmds.length) { continue; } 71 | selections = selections.concat(cmds); 72 | // visual label to divide up the jack sub commands 73 | selections.push({label: '$(kebab-horizontal)', description: ''}); 74 | } 75 | // Remove last divider 76 | selections.pop(); 77 | 78 | // Display full list of all commands and execute selected target. 79 | let result = await vscode.window.showQuickPick(selections); 80 | if (undefined === result || undefined === result.target) { return; } 81 | await result.target(); 82 | })); 83 | 84 | // Initialize tree views 85 | ext.connectionsTree = new ConnectionsTree(); 86 | ext.pipelineTree = new PipelineTree(); 87 | ext.jobTree = new JobTree(); 88 | ext.queueTree = new QueueTree(); 89 | ext.nodeTree = new NodeTree(); 90 | } 91 | 92 | export function deactivate() {} 93 | -------------------------------------------------------------------------------- /COMMANDS.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | ## Jack Commands 4 | 5 | `extension.jenkins-jack.jacks` 6 | 7 | > Opens a quickpick of all available commands for this extensions. Default hot-key is `ctrl+shift+j`. 8 | 9 | `extension.jenkins-jack.pipeline` 10 | 11 | `extension.jenkins-jack.scriptConsole` 12 | 13 | `extension.jenkins-jack.build` 14 | 15 | `extension.jenkins-jack.job` 16 | 17 | `extension.jenkins-jack.node` 18 | 19 | > Opens up each Jack's list of commands. 20 | 21 | `extension.jenkins-jack.pipeline.execute` 22 | 23 | > Executes the active Groovy file as a pipeline script on the targeted Jenkins. 24 | 25 | `extension.jenkins-jack.pipeline.sharedLibrary` 26 | 27 | > Provides a list of official Pipeline steps from the Shares Library and global variables. 28 | 29 | `extension.jenkins-jack.scriptConsole.execute` 30 | 31 | > Starts the flow for executing the active editor's groovy script as a system/node console script (script console)." 32 | 33 | `extension.jenkins-jack.build.abort` 34 | 35 | > Sends an abort signal to the host for one or more build. 36 | 37 | `extension.jenkins-jack.build.open` 38 | 39 | > Opens the targeted builds in the user's browser. 40 | 41 | `extension.jenkins-jack.build.delete` 42 | 43 | > Deletes a one or more builds from the Jenkins host. 44 | 45 | `extension.jenkins-jack.build.downloadLog` 46 | 47 | > Select a job and build to download the log. 48 | 49 | `extension.jenkins-jack.build.downloadReplayScript` 50 | 51 | > Pulls a pipeline replay script of a previous build into the editor. 52 | 53 | `extension.jenkins-jack.job.delete` 54 | 55 | > Disables targeted jobs on the remote Jenkins. 56 | 57 | `extension.jenkins-jack.job.disable` 58 | 59 | > Disables targeted jobs on the remote Jenkins. 60 | 61 | `extension.jenkins-jack.job.enable` 62 | 63 | > Enables targeted jobs on the remote Jenkins. 64 | 65 | `extension.jenkins-jack.job.open` 66 | 67 | > Opens the targeted jobs in the user's browser. 68 | 69 | `extension.jenkins-jack.node.setOffline` 70 | 71 | > Mark targeted nodes offline with a message. 72 | 73 | `extension.jenkins-jack.node.setOnline` 74 | 75 | > Mark targeted nodes online. 76 | 77 | `extension.jenkins-jack.node.disconnect` 78 | 79 | > Disconnects targeted nodes from the host. 80 | 81 | `extension.jenkins-jack.node.open` 82 | 83 | > Opens the targeted nodes in the user's browser. 84 | 85 | `extension.jenkins-jack.node.updateLabels` 86 | 87 | > Update targeted nodes' assigned labels. 88 | 89 | `extension.jenkins-jack.connections.select` 90 | 91 | > Select Jenkins host connection to connect to. 92 | 93 | `extension.jenkins-jack.connections.add` 94 | 95 | > Starts the flow for adding a Jenkins host connection. 96 | 97 | `extension.jenkins-jack.connections.edit` 98 | 99 | > Starts the flow for editing and existing Jenkins host connection. 100 | 101 | `extension.jenkins-jack.connections.delete` 102 | 103 | > Delete a Jenkins host connection. 104 | 105 | ## Tree Commands 106 | 107 | `extension.jenkins-jack.tree.pipeline.openScript` 108 | 109 | `extension.jenkins-jack.tree.pipeline.openScriptConfig` 110 | 111 | `extension.jenkins-jack.tree.pipeline.pullJobScript` 112 | 113 | `extension.jenkins-jack.tree.pipeline.pullReplayScript` 114 | 115 | `extension.jenkins-jack.tree.pipeline.refresh` 116 | 117 | `extension.jenkins-jack.tree.pipeline.addLink` 118 | 119 | `extension.jenkins-jack.tree.pipeline.removeLink` 120 | 121 | `extension.jenkins-jack.tree.node.refresh` 122 | 123 | `extension.jenkins-jack.tree.job.refresh` 124 | 125 | `extension.jenkins-jack.tree.connections.settings` 126 | 127 | `extension.jenkins-jack.tree.connections.refresh` 128 | -------------------------------------------------------------------------------- /src/connectionsTree.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ext } from './extensionVariables'; 3 | import { JenkinsConnection } from './jenkinsConnection'; 4 | import { filepath } from './utils'; 5 | 6 | export class ConnectionsTree { 7 | private readonly _treeView: vscode.TreeView; 8 | private readonly _treeViewDataProvider: ConnectionsTreeProvider; 9 | 10 | public constructor() { 11 | this._treeViewDataProvider = new ConnectionsTreeProvider(); 12 | this._treeView = vscode.window.createTreeView('connectionsTree', { treeDataProvider: this._treeViewDataProvider, canSelectMany: false }); 13 | this._treeView.onDidChangeVisibility((e: vscode.TreeViewVisibilityChangeEvent) => { 14 | if (e.visible) { this.refresh(); } 15 | }); 16 | 17 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.connections.settings', async () => { 18 | await vscode.commands.executeCommand('workbench.action.openSettingsJson'); 19 | })); 20 | 21 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.connections.refresh', () => { 22 | this.refresh(); 23 | ext.pipelineTree.refresh(); 24 | ext.jobTree.refresh(); 25 | ext.nodeTree.refresh(); 26 | ext.queueTree.refresh(); 27 | })); 28 | } 29 | 30 | public refresh() { 31 | this._treeView.title = `Jenkins Connections (${ext.connectionsManager.host.connection.name})`; 32 | this._treeViewDataProvider.refresh(); 33 | } 34 | } 35 | 36 | export class ConnectionsTreeProvider implements vscode.TreeDataProvider { 37 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 38 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 39 | 40 | public constructor() { 41 | this.updateSettings(); 42 | } 43 | 44 | private updateSettings() { 45 | } 46 | 47 | refresh(): void { 48 | this._onDidChangeTreeData.fire(undefined); 49 | } 50 | 51 | getTreeItem(element: ConnectionsTreeItem): ConnectionsTreeItem { 52 | return element; 53 | } 54 | 55 | getChildren(element?: ConnectionsTreeItem): Thenable { 56 | return new Promise(async resolve => { 57 | let config = vscode.workspace.getConfiguration('jenkins-jack.jenkins'); 58 | let list = []; 59 | for (let c of config.connections) { 60 | list.push(new ConnectionsTreeItem(c.name, JenkinsConnection.fromJSON(c))); 61 | } 62 | resolve(list); 63 | }); 64 | } 65 | } 66 | 67 | export class ConnectionsTreeItem extends vscode.TreeItem { 68 | constructor( 69 | public readonly label: string, 70 | public readonly connection: JenkinsConnection 71 | ) { 72 | super(label, vscode.TreeItemCollapsibleState.None); 73 | 74 | this.contextValue = connection.active ? 'connectionsTreeItemActive' : 'connectionsTreeItemInactive'; 75 | 76 | let iconPrefix = connection.active ? 'active' : 'inactive'; 77 | this.iconPath = { 78 | light: filepath('images', `${iconPrefix}-light.svg`), 79 | dark: filepath('images', `${iconPrefix}-dark.svg`), 80 | }; 81 | } 82 | 83 | // @ts-ignore 84 | get tooltip(): string { 85 | return ''; 86 | } 87 | 88 | // @ts-ignore 89 | get description(): string { 90 | let description = this.connection.uri; 91 | description += null != this.connection.folderFilter && '' != this.connection.folderFilter ? 92 | ` (${this.connection.folderFilter})` : 93 | ''; 94 | description += ` (${this.connection.username})`; 95 | return description; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | All commands can either be handled in the quick-pick command menu (`ctrl+shift+j`) or through the Jenkins Jack Views (bow icon on the left bar). 4 | 5 | ## Setting up a Connection 6 | 7 | The preferred method of authentication for the extension uses a generated [API token](https://www.jenkins.io/blog/2018/07/02/new-api-token-system/) which can be made by visiting your user configuration (e.g. `https://jenkins-server/user/tabeyti/configure`) 8 | 9 | ![setup](images/doc/demo_setup.gif) 10 | 11 | Host connections can be added through the __Connection: Add__ quick-pick command or the Connection Tree View. Inputs needed: 12 | * A unique name for this connection (e.g. `myconn`) 13 | * The base Jenkins address with `http/https` prefix 14 | * Your user name for the Jenkins server 15 | * The API token you generated for your user (stored on system key-chain) 16 | * (Optional) A folder path on the Jenkins to filter job queries and commands. Can be used for performance (limits number of jobs queried) or for quality-of-life when working within a specific location on Jenkins. 17 | * (Optional) CSRF projection for your connection (default true). __Only disable this for older [Jenkins versions](https://www.jenkins.io/doc/book/security/csrf-protection/)__ (pre 2.222) if you experience issues creating/editing jobs, builds, and nodes. 18 | 19 | Connection data is stored in the `settings.json`, but passwords are stored on the user's local system key-store. Service name for passwords are of the format `jenkins-jack:` 20 | 21 | ```javascript 22 | "jenkins-jack.jenkins.connections": [ 23 | { 24 | // Unique name for your Jenkins connection 25 | "name": "localhost", 26 | 27 | // Host and account name 28 | "uri": "http://127.0.0.1:8080/jenkins", 29 | "username": "", 30 | 31 | // (Optional) Folder path on the Jenkins for filtering job queries 32 | "folderFilter": "", 33 | 34 | // (Optional) Flag to disable CSRF protection for older Jenkins' (default is true) 35 | "crumbIssuer": true 36 | 37 | // Flag indicating whether the connection is active. 38 | // NOTE: Avoid setting this manually and use the extension commands for selecting an active connection 39 | "active": true 40 | }, 41 | ... 42 | ] 43 | ``` 44 | 45 | --- 46 | 47 | ## Creating a Pipeline 48 | 49 | ![pipeline](images/doc/demo_pipelinecreate.gif) 50 | * Create a Pipeline job and local script through the quick-pick command or the Tree View. User can select root or a Jenkins Folder job to create the Pipeline under. 51 | 52 | --- 53 | 54 | ## Executing a Pipeline 55 | 56 | ![pipeline](images/doc/demo_pipelinerun.gif) 57 | 58 | * Run a pipeline script from a local Groovy file on your machine 59 | * Pull a job or replay script from the host in the Pipeline Tree View, creating a link between the saved script and the host's job for easy access and execution in the Pipeline Tree View 60 | * Link a Pipeline job found on the host to an already existing local script for easy access and execution 61 | 62 | --- 63 | 64 | ## Execute a Pipeline with Build Parameters 65 | 66 | ![pipeline](images/doc/demo_pipelineparams.gif) 67 | 68 | * A user can modify build input/parameters in the `..config.json` config file local to the script (created on pipeline execution). You can also access script config quickly through the Pipeline Tree View context menu 69 | * Interactive input can be enabled in settings to prompt a user for values on each build parameter (only supports Jenkins default parameter types) during Pipeline execution 70 | 71 | --- 72 | 73 | ## Query Builds 74 | 75 | * Use VSCode's quick-pick search with wild-card support to query builds by date, result, duration, and description for any build operation (e.g. open, delete, download log, etc.) 76 | * Limit or expand the number of builds to retrieve for performance or more search results 77 | 78 | ![build](images/doc/demo_build_query.gif) 79 | 80 | ## Job and Build Management 81 | 82 | > TODO 83 | -------------------------------------------------------------------------------- /src/outputProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ext } from './extensionVariables'; 3 | 4 | export class OutputPanel implements vscode.OutputChannel { 5 | name: string; 6 | public readonly uri: vscode.Uri; 7 | 8 | private _text: string; 9 | private _activeEditor: vscode.TextEditor | undefined; 10 | private _defaultViewColumn: any; 11 | 12 | constructor(uri: vscode.Uri) { 13 | this.uri = uri; 14 | 15 | this.updateSettings(); 16 | vscode.workspace.onDidChangeConfiguration(event => { 17 | if (event.affectsConfiguration('jenkins-jack.outputView.panel')) { 18 | this.updateSettings(); 19 | } 20 | }); 21 | } 22 | 23 | /** 24 | * Updates the settings for this editor. 25 | */ 26 | private updateSettings() { 27 | let config = vscode.workspace.getConfiguration('jenkins-jack.outputView.panel'); 28 | this._defaultViewColumn = vscode.ViewColumn[config.defaultViewColumn]; 29 | } 30 | 31 | /** 32 | * Inherited from vscode.OutputChannel 33 | */ 34 | public async show() { 35 | let editor = vscode.window.visibleTextEditors.find((e: vscode.TextEditor) => 36 | e.document.uri.scheme === ext.outputPanelProvider.scheme && 37 | e.document.uri.path === this.uri.path); 38 | 39 | // Only display the default view column for the editor if the editor 40 | // isn't already shown 41 | let viewColumn = (undefined !== editor) ? editor.viewColumn : this._defaultViewColumn; 42 | 43 | let document = await vscode.workspace.openTextDocument(this.uri); 44 | this._activeEditor = await vscode.window.showTextDocument(document, viewColumn, false); 45 | 46 | // TODO: I don't know why this needs to be called to create a tab, but it does 47 | // await vscode.commands.executeCommand('vscode.open', this.uri); 48 | await vscode.languages.setTextDocumentLanguage(document, 'pipeline-log'); 49 | } 50 | 51 | public async append(text: string) { 52 | if (undefined === this._activeEditor) { 53 | return; 54 | } 55 | this._text += text; 56 | ext.outputPanelProvider.update(this.uri); 57 | } 58 | 59 | public async appendLine(value: string) { 60 | await this.append(`${value}\n`); 61 | } 62 | 63 | public hide(): void { 64 | throw new Error("Method not implemented."); 65 | } 66 | 67 | public dispose(): void { 68 | this._text = ''; 69 | } 70 | 71 | public clear() { 72 | this._text = ''; 73 | ext.outputPanelProvider.update(this.uri); 74 | } 75 | 76 | public text(): string { 77 | return this._text; 78 | } 79 | } 80 | 81 | export class OutputPanelProvider implements vscode.TextDocumentContentProvider { 82 | private _eventEmitter: vscode.EventEmitter; 83 | private _panelMap: Map; 84 | 85 | public constructor() { 86 | this._eventEmitter = new vscode.EventEmitter(); 87 | 88 | ext.context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(this.scheme, this)); 89 | 90 | this._panelMap = new Map(); 91 | } 92 | 93 | public get scheme(): string { 94 | return 'jenkins-jack'; 95 | } 96 | 97 | public get(key: string): OutputPanel { 98 | if (!this._panelMap.has(key)) { 99 | this._panelMap.set(key, new OutputPanel(vscode.Uri.parse(`${this.scheme}:${key}`))); 100 | } 101 | 102 | // @ts-ignore 103 | return this._panelMap.get(key); 104 | } 105 | 106 | 107 | public update(uri: vscode.Uri) { 108 | this._eventEmitter.fire(uri); 109 | } 110 | 111 | get onDidChange(): vscode.Event { 112 | return this._eventEmitter.event; 113 | } 114 | 115 | async provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): Promise { 116 | if (uri.scheme !== this.scheme) { 117 | return ''; 118 | } 119 | let panel = this.get(uri.path); 120 | // @ts-ignore 121 | return panel.text(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/queueTree.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ext } from './extensionVariables'; 3 | import { msToTime, sleep } from './utils'; 4 | 5 | export class QueueTree { 6 | private readonly _treeView: vscode.TreeView; 7 | private readonly _treeViewDataProvider: QueueTreeProvider; 8 | 9 | public constructor() { 10 | this._treeViewDataProvider = new QueueTreeProvider(); 11 | this._treeView = vscode.window.createTreeView('queueTree', { showCollapseAll: true, treeDataProvider: this._treeViewDataProvider, canSelectMany: true }); 12 | this._treeView.onDidChangeVisibility((e: vscode.TreeViewVisibilityChangeEvent) => { 13 | if (e.visible) { this.refresh(); } 14 | }); 15 | 16 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.queue.refresh', () => { 17 | this.refresh(); 18 | })); 19 | } 20 | 21 | // @ts-ignore 22 | public refresh(delayMs?: int = 0) { 23 | sleep(delayMs*1000).then(() => { 24 | this._treeView.title = `Queue Items (${ext.connectionsManager.host.connection.name})`; 25 | this._treeViewDataProvider.refresh(); 26 | }) 27 | } 28 | } 29 | 30 | export class QueueTreeProvider implements vscode.TreeDataProvider { 31 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 32 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 33 | private _cancelTokenSource: vscode.CancellationTokenSource; 34 | private _QueueTreeItems: QueueTreeItem[] = []; 35 | public get QueueTreeItems(): QueueTreeItem[] { 36 | return this._QueueTreeItems; 37 | } 38 | 39 | public constructor() { 40 | this._cancelTokenSource = new vscode.CancellationTokenSource(); 41 | this.updateSettings(); 42 | } 43 | 44 | private updateSettings() { 45 | this.refresh(); 46 | } 47 | 48 | refresh(): void { 49 | this._cancelTokenSource.cancel(); 50 | this._cancelTokenSource.dispose(); 51 | this._cancelTokenSource = new vscode.CancellationTokenSource(); 52 | this._onDidChangeTreeData.fire(undefined); 53 | } 54 | 55 | getTreeItem(element: QueueTreeItem): QueueTreeItem { 56 | return element; 57 | } 58 | 59 | getParent(element?: QueueTreeItem): QueueTreeItem | null { 60 | if (!element?.parent) { 61 | return null 62 | } 63 | return element.parent; 64 | } 65 | 66 | getChildren(element?: QueueTreeItem): Thenable { 67 | return new Promise(async resolve => { 68 | let list = []; 69 | if (!ext.connectionsManager.connected) { 70 | resolve(list); 71 | return; 72 | } 73 | 74 | let items = await ext.connectionsManager.host.getQueueItems(this._cancelTokenSource.token); 75 | if (undefined === items) { 76 | resolve([]); 77 | return; 78 | } 79 | for (let item of items) { 80 | list.push(new QueueTreeItem(item.name, vscode.TreeItemCollapsibleState.None, item)) 81 | } 82 | resolve(list); 83 | }); 84 | } 85 | } 86 | 87 | export class QueueTreeItem extends vscode.TreeItem { 88 | constructor( 89 | public readonly label: string, 90 | public readonly treeItemState: vscode.TreeItemCollapsibleState, 91 | public readonly queueItem: any, 92 | public readonly parent?: QueueTreeItem 93 | ) { 94 | super(label, treeItemState); 95 | 96 | this.contextValue = 'queue-item'; 97 | if (this.queueItem.stuck) { 98 | this.iconPath = new vscode.ThemeIcon('error'); 99 | this.contextValue += "-stuck" 100 | } else { 101 | this.iconPath = new vscode.ThemeIcon('watch'); 102 | this.contextValue += "-blocked" 103 | } 104 | } 105 | 106 | // @ts-ignore 107 | get tooltip(): string { 108 | return this.queueItem.detail; 109 | } 110 | 111 | // @ts-ignore 112 | get description(): string { 113 | return `(${msToTime(Date.now() - this.queueItem.inQueueSince)}) ${this.queueItem.why}`; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 43 | 46 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 64 | 71 | 72 | 77 | 84 | 85 | 90 | 95 | J 106 | J 117 | 118 | 119 | -------------------------------------------------------------------------------- /images/build-inprogress-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/build-inprogress-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/snippets.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { PipelineStepDoc } from './stepdoc'; 3 | import { ext } from './extensionVariables'; 4 | 5 | export class PipelineSnippets { 6 | public completionItems: Array; 7 | private enabled: Boolean; 8 | private stepDocs: Array; 9 | 10 | /** 11 | * Constructor. 12 | */ 13 | constructor() { 14 | 15 | ext.context.subscriptions.push(vscode.languages.registerCompletionItemProvider('groovy', { 16 | provideCompletionItems( 17 | document: vscode.TextDocument, 18 | position: vscode.Position, 19 | token: vscode.CancellationToken, 20 | context: vscode.CompletionContext) { 21 | return ext.pipelineSnippets.completionItems; 22 | } 23 | })); 24 | 25 | vscode.workspace.onDidChangeConfiguration(event => { 26 | if (event.affectsConfiguration('jenkins-jack.snippets') || 27 | event.affectsConfiguration('jenkins-jack.jenkins.connections')) { 28 | this.updateSettings(); 29 | } 30 | }); 31 | 32 | this.updateSettings(); 33 | } 34 | 35 | public updateSettings() { 36 | this.completionItems = new Array(); 37 | this.stepDocs = new Array(); 38 | 39 | let config = vscode.workspace.getConfiguration('jenkins-jack.snippets'); 40 | this.enabled = config.enabled; 41 | this.refresh(); 42 | } 43 | 44 | /** 45 | * Refreshes the remote Jenkins' Pipeline Steps documentation, 46 | * parsed from the GDSL. 47 | */ 48 | public async refresh() { 49 | if (!this.enabled) { return; } 50 | ext.logger.info('refresh - Refreshing Pipeline step auto-completions.'); 51 | 52 | this.completionItems = new Array(); 53 | this.stepDocs = new Array(); 54 | 55 | // Parse each GDSL line for a 'method' signature. 56 | // This is a Pipeline Sep. 57 | let gdsl = await ext.connectionsManager.host.get('pipeline-syntax/gdsl'); 58 | if (undefined === gdsl) { return; } 59 | 60 | let lines = String(gdsl).split(/\r?\n/); 61 | lines.forEach(line => { 62 | var match = line.match(/method\((.*?)\)/); 63 | if (null === match || match.length <= 0) { 64 | return; 65 | } 66 | this.stepDocs.push(this.parseMethodLine(line)); 67 | }); 68 | 69 | // Populate completion items. 70 | for (let step of this.stepDocs) { 71 | let item = new vscode.CompletionItem(step.name, vscode.CompletionItemKind.Snippet); 72 | item.detail = step.getSignature(); 73 | item.documentation = step.doc; 74 | item.insertText = step.getSnippet(); 75 | this.completionItems.push(item); 76 | } 77 | } 78 | 79 | /** 80 | * Parses a Pipeline step "method(...)" line from the GDSL. 81 | * @param line The method line. 82 | */ 83 | public parseMethodLine(line: string): PipelineStepDoc { 84 | let name = ""; 85 | let doc = ""; 86 | let params = new Map(); 87 | 88 | let match = line.match(/method\(name:\s+'(.*?)',.* params: \[(.*?)],.* doc:\s+'(.*)'/); 89 | if (null !== match && match.length >= 0) { 90 | name = match[1]; 91 | doc = match[3]; 92 | 93 | // Parse step parameters. 94 | params = new Map(); 95 | match[2].split(',').forEach(p => { 96 | let pcomps = p.split(":"); 97 | if ("" === pcomps[0]) { 98 | return; 99 | } 100 | params.set(pcomps[0], pcomps[1].replace("'", "").replace("'", "").trim()); 101 | }); 102 | } 103 | else { 104 | let match = line.match(/method\(name:\s+'(.*?)',.*namedParams: \[(.*?)\],.* doc:\s+'(.*)'/); 105 | if (null === match) { 106 | throw Error("Base match regex is wrong."); 107 | } 108 | if (match.length >= 0) { 109 | name = match[1]; 110 | doc = match[3]; 111 | 112 | // Parse step parameters. 113 | params = new Map(); 114 | let rawParams = match[2].split(", parameter"); 115 | rawParams.forEach(rp => { 116 | let tm = rp.match(/.*name:\s+'(.*?)', type:\s+'(.*?)'.*/); 117 | if (null === tm || tm.length <= 0) { return; } 118 | params.set(tm[1], tm[2]); 119 | }); 120 | } 121 | } 122 | return new PipelineStepDoc(name, doc, params); 123 | } 124 | } -------------------------------------------------------------------------------- /src/scriptConsoleJack.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ext } from './extensionVariables'; 3 | import { JackBase } from './jack'; 4 | import { getValidEditor } from './utils'; 5 | import { NodeTreeItem } from './nodeTree'; 6 | import { SelectionFlows } from './selectionFlows'; 7 | 8 | export class ScriptConsoleJack extends JackBase { 9 | 10 | constructor() { 11 | super('Script Console Jack', 'extension.jenkins-jack.scriptConsole'); 12 | 13 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.scriptConsole.execute', async (item?: any, items?: any[]) => { 14 | 15 | // If View items were passed in, grab the agent names, otherwise prompt user for agent selection 16 | if (item instanceof NodeTreeItem) { 17 | items = !items ? [item.node.displayName] : items.map((item: NodeTreeItem) => item.node.displayName); 18 | } 19 | else { 20 | let nodes = await SelectionFlows.nodes(undefined, true, 'Select one or more nodes to execute your console script on', true); 21 | if (undefined === nodes) { return; } 22 | items = nodes.map((n: any) => n.displayName); 23 | } 24 | 25 | // Verify with the user that they want to run the script on the targeted agents 26 | if (undefined === items) { return undefined; } 27 | let r = await this.showInformationModal( 28 | `Are you sure you want run the active script on these agents?\n\n${items.join('\n')}`, 29 | { title: "Yes"} ); 30 | if (undefined === r) { return undefined; } 31 | 32 | await this.execute(items); 33 | })); 34 | } 35 | 36 | public get commands(): any[] { 37 | return [{ 38 | label: "$(terminal) Script Console: Execute", 39 | description: "Executes the current view's groovy script as a system/node console script (script console).", 40 | target: async () => vscode.commands.executeCommand('extension.jenkins-jack.scriptConsole.execute') 41 | }]; 42 | } 43 | 44 | private async execute(targetNodes?: any[]) { 45 | 46 | // Validate it's valid groovy source and grab it from the active editor 47 | var editor = getValidEditor(); 48 | if (undefined === editor) { 49 | this.showWarningMessage('Must have a file open with a supported language id to use this command.'); 50 | return; 51 | } 52 | let source = editor.document.getText(); 53 | 54 | vscode.window.withProgress({ 55 | location: vscode.ProgressLocation.Notification, 56 | title: `Console Script(s)`, 57 | cancellable: true 58 | }, async (progress, token) => { 59 | token.onCancellationRequested(() => { 60 | this.showWarningMessage(`User canceled script console execute.`); 61 | }); 62 | 63 | // Builds a list of console script http requests across the list of targeted machines 64 | // and awaits across all. 65 | let tasks = []; 66 | progress.report({ increment: 50, message: "Executing on target machine(s)" }); 67 | if (undefined === targetNodes) { return undefined; } 68 | for (let m of targetNodes) { 69 | let promise = undefined; 70 | if ('master' === m) { 71 | promise = new Promise(async (resolve) => { 72 | let result = await ext.connectionsManager.host.runConsoleScript(source, undefined, token); 73 | return resolve({ node: 'master', output: result }); 74 | }); 75 | } 76 | else { 77 | promise = new Promise(async (resolve) => { 78 | let result = await ext.connectionsManager.host.runConsoleScript(source, m, token); 79 | return resolve({ node: m, output: result }); 80 | }); 81 | } 82 | tasks.push(promise); 83 | } 84 | let results = await Promise.all(tasks); 85 | 86 | // Iterate over the result list, printing the name of the 87 | // machine and it's output. 88 | this.outputChannel.clear(); 89 | this.outputChannel.show(); 90 | for (let r of results as any[]) { 91 | this.outputChannel.appendLine(this.barrierLine); 92 | this.outputChannel.appendLine(r.node); 93 | this.outputChannel.appendLine(''); 94 | this.outputChannel.appendLine(r.output); 95 | this.outputChannel.appendLine(this.barrierLine); 96 | } 97 | progress.report({ increment: 50, message: `Output retrieved. Displaying in OUTPUT channel...` }); 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/jobTree.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ext } from './extensionVariables'; 3 | import { JobType } from './jobType'; 4 | import { filepath, toDateString } from './utils'; 5 | 6 | export class JobTree { 7 | private readonly _treeView: vscode.TreeView; 8 | private readonly _treeViewDataProvider: JobTreeProvider; 9 | 10 | public constructor() { 11 | this._treeViewDataProvider = new JobTreeProvider(); 12 | this._treeView = vscode.window.createTreeView('jobTree', { treeDataProvider: this._treeViewDataProvider, canSelectMany: true }); 13 | this._treeView.onDidChangeVisibility((e: vscode.TreeViewVisibilityChangeEvent) => { 14 | if (e.visible) { this.refresh(); } 15 | }); 16 | 17 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.job.refresh', (content: any) => { 18 | this.refresh(); 19 | })); 20 | } 21 | 22 | public refresh() { 23 | this._treeView.title = `Jobs (${ext.connectionsManager.host.connection.name})`; 24 | this._treeViewDataProvider.refresh(); 25 | } 26 | } 27 | 28 | export class JobTreeProvider implements vscode.TreeDataProvider { 29 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 30 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 31 | private _cancelTokenSource: vscode.CancellationTokenSource; 32 | 33 | private _treeConfig: any; 34 | private _jobTreeConfig: any; 35 | 36 | public constructor() { 37 | this._cancelTokenSource = new vscode.CancellationTokenSource(); 38 | 39 | vscode.workspace.onDidChangeConfiguration(event => { 40 | if (event.affectsConfiguration('jenkins-jack.tree') || event.affectsConfiguration('jenkins-jack.job.tree')) { 41 | this.updateSettings(); 42 | } 43 | }); 44 | 45 | this.updateSettings(); 46 | } 47 | 48 | private updateSettings() { 49 | this._treeConfig = vscode.workspace.getConfiguration('jenkins-jack.tree'); 50 | this._jobTreeConfig = vscode.workspace.getConfiguration('jenkins-jack.job.tree'); 51 | this.refresh(); 52 | } 53 | 54 | refresh(): void { 55 | this._cancelTokenSource.cancel(); 56 | this._cancelTokenSource.dispose(); 57 | this._cancelTokenSource = new vscode.CancellationTokenSource(); 58 | this._onDidChangeTreeData.fire(undefined); 59 | } 60 | 61 | getTreeItem(element: JobTreeItem): JobTreeItem { 62 | return element; 63 | } 64 | 65 | getChildren(element?: JobTreeItem): Thenable { 66 | return new Promise(async resolve => { 67 | let list = []; 68 | if (!ext.connectionsManager.connected) { 69 | resolve(list); 70 | return; 71 | } 72 | 73 | if (element) { 74 | let builds = await ext.connectionsManager.host.getBuildsWithProgress(element.job, this._jobTreeConfig.numBuilds, this._cancelTokenSource.token); 75 | for (let build of builds) { 76 | let label = `${build.number} ${toDateString(build.timestamp)}`; 77 | list.push(new JobTreeItem(label, JobTreeItemType.Build, vscode.TreeItemCollapsibleState.None, element.job, build)); 78 | } 79 | } else { 80 | let jobs = await ext.connectionsManager.host.getJobs(null, { token: this._cancelTokenSource.token }); 81 | jobs = jobs.filter((job: any) => job.type !== JobType.Folder); 82 | 83 | for(let job of jobs) { 84 | let label = job.fullName.replace(/\//g, this._treeConfig.directorySeparator); 85 | let jobTreeItem = new JobTreeItem(label, JobTreeItemType.Job, vscode.TreeItemCollapsibleState.Collapsed,job); 86 | list.push(jobTreeItem); 87 | } 88 | } 89 | resolve(list); 90 | }); 91 | } 92 | } 93 | 94 | export enum JobTreeItemType { 95 | Job = 'Job', 96 | Build = 'Build' 97 | } 98 | 99 | export class JobTreeItem extends vscode.TreeItem { 100 | constructor( 101 | public readonly label: string, 102 | public readonly type: JobTreeItemType, 103 | public readonly treeItemState: vscode.TreeItemCollapsibleState, 104 | public readonly job: any, 105 | public readonly build?: any 106 | ) { 107 | super(label, treeItemState); 108 | 109 | let iconPrefix = 'active'; 110 | if (JobTreeItemType.Job === type) { 111 | this.contextValue = 'job-active'; 112 | 113 | if (!job.buildable) { 114 | this.contextValue += '-disabled'; 115 | iconPrefix = 'inactive'; 116 | } 117 | } 118 | else { 119 | this.contextValue = [JobType.Multi, JobType.Org, JobType.Pipeline].includes(job.type) ? 'build-pipeline' : 'build'; 120 | 121 | if (this.build.building) { 122 | iconPrefix = 'build-inprogress'; 123 | this.contextValue += '-inprogress'; 124 | } 125 | else if ('FAILURE' === build.result) { 126 | iconPrefix = 'build-bad'; 127 | } else if ('ABORTED' === build.result) { 128 | iconPrefix = 'build-aborted'; 129 | } else if ('UNSTABLE' === build.result) { 130 | iconPrefix = 'build-unstable'; 131 | } else { 132 | iconPrefix = 'build-good'; 133 | } 134 | } 135 | this.iconPath = { 136 | light: filepath('images', `${iconPrefix}-light.svg`), 137 | dark: filepath('images', `${iconPrefix}-dark.svg`), 138 | }; 139 | } 140 | 141 | // @ts-ignore 142 | get tooltip(): string { 143 | if (JobTreeItemType.Job === this.type) { 144 | return (undefined === this.job.description || '' === this.job.description) ? 145 | this.label : 146 | `${this.label} - ${this.job.description}`; 147 | } 148 | else { 149 | return this.build.building ? 150 | `${this.label}: IN PROGRESS` : 151 | `${this.label}: ${this.build.result}`; 152 | } 153 | } 154 | 155 | // @ts-ignore 156 | get description(): string { 157 | return JobTreeItemType.Job === this.type ? this.job.description : this.build.description; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/selectionFlows.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ext } from './extensionVariables'; 3 | 4 | export class SelectionFlows { 5 | 6 | constructor() { } 7 | 8 | /** 9 | * Provides a quick pick selection of one or more jobs, returning the selected items. 10 | * @param filter A function for filtering the job list retrieved from the Jenkins host. 11 | * @param canPickMany Optional flag for retrieving more than one job in the selection. 12 | * @param message Optional help message to display to the user. 13 | */ 14 | public static async jobs( 15 | filter?: ((job: any) => boolean), 16 | canPickMany?: boolean, 17 | message?: string): Promise { 18 | 19 | message = message ?? 'Select a job to grab builds from'; 20 | 21 | let jobs = await ext.connectionsManager.host.getJobs(); 22 | if (undefined === jobs) { return undefined; } 23 | if (filter) { 24 | jobs = jobs.filter(filter); 25 | } 26 | for (let job of jobs) { job.label = job.fullName; } 27 | 28 | let selectedJobs = await vscode.window.showQuickPick(jobs, { 29 | canPickMany: canPickMany, 30 | ignoreFocusOut: true, 31 | placeHolder: message, 32 | matchOnDetail: true, 33 | matchOnDescription: true 34 | }); 35 | if (undefined === selectedJobs) { return undefined; } 36 | return selectedJobs; 37 | } 38 | 39 | /** 40 | * Provides a quick pick selection of one or more builds, returning the selected items. 41 | * @param job The target job for retrieval the builds. 42 | * @param canPickMany Optional flag for retrieving more than one build in the selection. 43 | * @param message Optional help message to display to the user. 44 | */ 45 | public static async builds( 46 | job?: any, 47 | filter?: ((build: any) => boolean), 48 | canPickMany?: boolean, 49 | message?: string): Promise { 50 | 51 | message = message ?? 'Select a build.'; 52 | 53 | // If job wasn't provided, prompt user to select one. 54 | job = job ?? (await SelectionFlows.jobs(undefined, false)); 55 | if (undefined == job) { return undefined;} 56 | 57 | // Get number of builds to retrieve, defaulting to 100 for performance. 58 | let numBuilds = await vscode.window.showInputBox({ 59 | ignoreFocusOut: true, 60 | placeHolder: 'Enter number of builds to retrieve', 61 | prompt: 'Number of builds to query on (NOTE: values over 100 will utilize the "allBuilds" field in the query, which may slow performance on the Jenkins server)', 62 | validateInput: text => { 63 | if (!/^\d+$/.test(text) || parseInt(text) <= 0 ) { return 'Must provide a number greater than 0.'} 64 | return undefined; 65 | }, 66 | value: '100' 67 | }); 68 | if (undefined == numBuilds) { return undefined; } 69 | 70 | // Ask what build they want to download. 71 | let builds = await ext.connectionsManager.host.getBuildsWithProgress(job, parseInt(numBuilds)); 72 | 73 | if (0 >= builds.length) { 74 | vscode.window.showWarningMessage(`No builds retrieved for "${job.fullName}"`); 75 | return undefined; 76 | } 77 | if (null != filter) { builds = builds.filter(filter); } 78 | let selections = await vscode.window.showQuickPick(builds, { 79 | canPickMany: canPickMany, 80 | ignoreFocusOut: true, 81 | placeHolder: message, 82 | matchOnDetail: true, 83 | matchOnDescription: true 84 | }) as any; 85 | if (undefined === selections) { return undefined; } 86 | return selections; 87 | } 88 | 89 | /** 90 | * Provides a quick pick selection of one or more nodes, returning the selected items. 91 | * @param filter A function for filtering the nodes retrieved from the Jenkins host. 92 | * @param canPickMany Optional flag for retrieving more than one node in the selection. 93 | * @param message Optional help message to display to the user. 94 | * @param includeMaster Optional flag for including master in the selection to the user. 95 | */ 96 | public static async nodes( 97 | filter?: ((node: any) => boolean), 98 | canPickMany?: boolean, 99 | message?: string, 100 | includeMaster?: boolean): Promise { 101 | 102 | message = message ?? 'Select a node.'; 103 | 104 | let nodes = await ext.connectionsManager.host.getNodes(); 105 | if (!includeMaster) { nodes.shift(); } 106 | if (undefined !== filter) { nodes = nodes.filter(filter); } 107 | if (undefined === nodes) { return undefined; } 108 | if (0 >= nodes.length) { 109 | vscode.window.showInformationMessage('No nodes found outside of "master"'); 110 | return undefined; 111 | } 112 | 113 | let selections = await vscode.window.showQuickPick(nodes, { 114 | canPickMany: canPickMany, 115 | ignoreFocusOut: true, 116 | placeHolder: message, 117 | matchOnDetail: true, 118 | matchOnDescription: true 119 | }) as any; 120 | if (undefined === selections) { return; } 121 | return selections; 122 | } 123 | 124 | /** 125 | * Provides a quick pick selection of one or more Jenkins Folders jobs, returning the selected folder names. 126 | * @param canPickMany Optional flag for retrieving more than one node in the selection. 127 | * @param message Optional help message to display to the user. 128 | * @param ignoreFolderFilter Optional flag for ignoring the folderFilter during folder retrieval. 129 | * @returns A list of Jenkins folder jobs. 130 | */ 131 | public static async folders( 132 | canPickMany?: boolean, 133 | message?: string, 134 | ignoreFolderFilter?: boolean): Promise { 135 | 136 | let folders = await ext.connectionsManager.host.getFolders(undefined, ignoreFolderFilter); 137 | folders = folders.map((f: any) => f.fullName ); 138 | 139 | let rootFolder = ext.connectionsManager.host.connection.folderFilter; 140 | rootFolder = (!ignoreFolderFilter && rootFolder) ? rootFolder : '.'; 141 | folders.unshift(rootFolder); 142 | 143 | let selection = await vscode.window.showQuickPick(folders, { 144 | canPickMany: canPickMany, 145 | ignoreFocusOut: true, 146 | placeHolder: message 147 | }); 148 | if (undefined === selection) { return undefined; } 149 | return selection; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/nodeTree.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ext } from './extensionVariables'; 3 | import { filepath, msToTime, sleep } from './utils'; 4 | 5 | export class NodeTree { 6 | private readonly _treeView: vscode.TreeView; 7 | private readonly _treeViewDataProvider: NodeTreeProvider; 8 | 9 | public constructor() { 10 | this._treeViewDataProvider = new NodeTreeProvider(); 11 | this._treeView = vscode.window.createTreeView('nodeTree', { showCollapseAll: true, treeDataProvider: this._treeViewDataProvider, canSelectMany: true }); 12 | this._treeView.onDidChangeVisibility((e: vscode.TreeViewVisibilityChangeEvent) => { 13 | if (e.visible) { this.refresh(); } 14 | }); 15 | 16 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.node.refresh', () => { 17 | this.refresh(); 18 | })); 19 | 20 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.node.expandAll', () => { 21 | for (let item of this._treeViewDataProvider.nodeTreeItems) { 22 | this._treeView.reveal(item, { expand: 1, select: false, focus: false } ) 23 | } 24 | })); 25 | } 26 | 27 | // @ts-ignore 28 | public refresh(delayMs?: int = 0) { 29 | sleep(delayMs*1000).then(() => { 30 | this._treeView.title = `Nodes (${ext.connectionsManager.host.connection.name})`; 31 | this._treeViewDataProvider.refresh(); 32 | }) 33 | } 34 | } 35 | 36 | export class NodeTreeProvider implements vscode.TreeDataProvider { 37 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 38 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 39 | private _cancelTokenSource: vscode.CancellationTokenSource; 40 | private _nodeTreeItems: NodeTreeItem[] = []; 41 | public get nodeTreeItems(): NodeTreeItem[] { 42 | return this._nodeTreeItems; 43 | } 44 | 45 | public constructor() { 46 | this._cancelTokenSource = new vscode.CancellationTokenSource(); 47 | this.updateSettings(); 48 | } 49 | 50 | private updateSettings() { 51 | this.refresh(); 52 | } 53 | 54 | refresh(): void { 55 | this._cancelTokenSource.cancel(); 56 | this._cancelTokenSource.dispose(); 57 | this._cancelTokenSource = new vscode.CancellationTokenSource(); 58 | this._onDidChangeTreeData.fire(undefined); 59 | } 60 | 61 | getTreeItem(element: NodeTreeItem): NodeTreeItem { 62 | return element; 63 | } 64 | 65 | getParent(element?: NodeTreeItem): NodeTreeItem | null { 66 | if (!element?.parent) { 67 | return null 68 | } 69 | return element.parent; 70 | } 71 | 72 | getChildren(element?: NodeTreeItem): Thenable { 73 | return new Promise(async resolve => { 74 | let list = []; 75 | if (!ext.connectionsManager.connected) { 76 | resolve(list); 77 | return; 78 | } 79 | 80 | if (element) { 81 | for (let e of element.node.executors) { 82 | let label = (!e.currentExecutable || e.currentExecutable.idle) ? 'Idle' : e.currentExecutable?.displayName; 83 | list.push(new NodeTreeItem(label, vscode.TreeItemCollapsibleState.None, element.node, e, element)); 84 | } 85 | } else { 86 | let nodes = await ext.connectionsManager.host.getNodes(this._cancelTokenSource.token); 87 | if (null == nodes) { 88 | resolve([]); 89 | return; 90 | } 91 | 92 | nodes = nodes?.filter((n: any) => n.displayName !== 'master'); 93 | this._nodeTreeItems = []; 94 | for (let n of nodes) { 95 | let nodeTreeItem = new NodeTreeItem(`${n.displayName}`, vscode.TreeItemCollapsibleState.Collapsed, n) 96 | this._nodeTreeItems.push(nodeTreeItem); 97 | list.push(nodeTreeItem); 98 | } 99 | } 100 | resolve(list); 101 | }); 102 | } 103 | } 104 | 105 | export class NodeTreeItem extends vscode.TreeItem { 106 | constructor( 107 | public readonly label: string, 108 | public readonly treeItemState: vscode.TreeItemCollapsibleState, 109 | public readonly node: any, 110 | public readonly executor?: any, 111 | public readonly parent?: NodeTreeItem 112 | ) { 113 | super(label, treeItemState); 114 | 115 | let iconPrefix = 'node-enabled'; 116 | this.contextValue = 'node-enabled'; 117 | if (!this.executor) { 118 | if (node.offline && node.temporarilyOffline) { 119 | iconPrefix = 'node-disabled'; 120 | this.contextValue = 'node-disabled'; 121 | } else if (node.offline) { 122 | iconPrefix = 'node-disconnected'; 123 | this.contextValue = 'node-disconnected'; 124 | } 125 | } else { 126 | iconPrefix = (!this.executor.idle) ? 'active' : 'inactive'; 127 | this.contextValue = `executor-${iconPrefix}`; 128 | } 129 | 130 | this.iconPath = { 131 | light: filepath('images', `${iconPrefix}-light.svg`), 132 | dark: filepath('images', `${iconPrefix}-dark.svg`), 133 | }; 134 | } 135 | 136 | // @ts-ignore 137 | get tooltip(): string { 138 | let tooltip = this.label; 139 | 140 | if (!this.executor) { 141 | if (this.node.temporarilyOffline) { 142 | tooltip += ' (OFFLINE)'; 143 | } else if (this.node.offline) { 144 | tooltip += ' (DISCONNECTED)'; 145 | } else { 146 | tooltip += ' (ONLINE)'; 147 | } 148 | 149 | if (this.node.temporarilyOffline) { 150 | tooltip = `${tooltip}\n${this.node.offlineCauseReason}`; 151 | } 152 | } else if (!this.executor.idle) { 153 | tooltip += ` (${msToTime(Date.now() - this.executor.currentExecutable.timestamp)})` 154 | } 155 | return tooltip; 156 | } 157 | 158 | // @ts-ignore 159 | get description(): string { 160 | let description = ''; 161 | 162 | if (this.executor) { 163 | if (!this.executor.idle) { 164 | description = `Duration: ${msToTime(Date.now() - this.executor.currentExecutable.timestamp)}`; 165 | } 166 | } else { 167 | description += this.node.description; 168 | if (this.node.temporarilyOffline) { 169 | description += ` (${this.node.offlineCauseReason})`; 170 | } 171 | } 172 | return description; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](images/doc/demo.gif) 2 | 3 | # Jenkins Jack 4 | 5 | Are you tired of bloated extensions with superfluous features that are so confusing to use, you'd rather do everything manually? 6 | 7 | I'm not! 8 | 9 | Jack into your Jenkins to streamline your Pipeline development and Jenkins management. Execute Pipeline scripts remotely with real-time syntax highlighted output, access Pipeline step auto-completions, pull Pipeline step documentation, run console groovy scripts across multiple agents, manage jobs/builds/agents, and more! 10 | 11 | Honestly, not that much more. 12 | 13 | ## Features 14 | 15 | * Pipeline Jack 16 | * Execute (with build parameters) 17 | * Stream syntax highlighted output to output channel 18 | * Abort executed pipeline 19 | * Update target pipeline job on host with script 20 | * Shared Library reference docs 21 | * Script Console Jack 22 | * Execute groovy console script at the System level or across one or more agents 23 | * Node (agent) Jack 24 | * Disable (with an offline message), enable, or disconnect one or more nodes 25 | * Update the labels on one more more nodes 26 | * Open agent in web browser 27 | * Job Jack 28 | * Disable, enable, delete, or view one or more jobs 29 | * Open job in web browser 30 | * Build Jack 31 | * Download a build log 32 | * Download a build's replay script 33 | * Delete one or more builds 34 | * Open build in web browser 35 | * Add, delete, edit, and select Jenkins host connections 36 | * Pipeline (GDSL) auto-completions for `groovy` files 37 | * Tree Views 38 | * Connection Tree 39 | * Add, edit, delete, and select your Jenkins host connections here 40 | * Pipeline Tree 41 | * Manage local scripts in relation to jobs on the targeted host 42 | * Pull job script from host 43 | * Pull replay script from build on host 44 | * Re-open your pulled script; association saved in `settings.json` 45 | * Job Tree 46 | * View jobs and builds on the host 47 | * Disable, enable, delete jobs and builds on the targeted host 48 | * Node (agent) Tree 49 | * View nodes on the host 50 | * Disable (with offline message), enable, disconnect nodes on the targeted host 51 | * Update one or more nodes labels 52 | 53 | ## Jacks! 54 | 55 | See [COMMANDS.md](COMMANDS.md) for a more comprehensive list of commands and their use. 56 | 57 | |Jack|Description|Command| 58 | |---|---|:---| 59 | |__Pipeline__|Remotely execute/abort/update Jenkins pipeline scripts from an open file with Groovy language id set, streaming syntax highlighted logs to the output console.|`extension.jenkins-jack.pipeline`| 60 | |__Script Console__|Remotely execute Console Groovy scripts through the Jenkins Script Console, targeting one or more agents.|`extension.jenkins-jack.scriptConsole`| 61 | |__Build__|Delete/abort builds, stream logs, and pull Pipeline replay scripts from your Jenkins host.|`extension.jenkins-jack.build`| 62 | |__Job__|Disable/enable/delete one or more jobs from your remote Jenkins.|`extension.jenkins-jack.job`| 63 | |__Node__|Disable/enable/disconnect one or more agents from your remote Jenkins. Mass update agent labels as well.|`extension.jenkins-jack.node`| 64 | 65 | Individual jacks can be mapped to hot keys as user sees fit. 66 | 67 | ## Views 68 | 69 | The extensions comes with UI/Views for interacting with all Jacks. The views can be found in the activity bar on the left hand side of the editor (bow icon): 70 | 71 | ![Views](images/doc/views.png) 72 | 73 | All commands a user can execute via the quickpick command list (`ctrl+shift+j`) can also be executed in the Views via context menu or buttons. 74 | 75 | For examples on interacting with the views, see [TUTORIAL.md](TUTORIAL.md). 76 | 77 | ## Auto-completions (faux snippets) 78 | 79 | From the selected remote Jenkins, the extension will pull, parse, and provide Pipeline steps as auto-completions from the Pipeline step definitions (GDSL). 80 | 81 | Any file in the editor with the Groovy language id set will have these completions (can be disabled via settings). 82 | 83 | ## Settings 84 | 85 | 86 | |Name |Description | 87 | | --- | ---------- | 88 | | `jenkins-jack.jenkins.connections` | List of jenkins connections to target when running commands. | 89 | | `jenkins-jack.jenkins.strictTls` | If unchecked, the extension will **not** check certificate validity when connecting through HTTPS | 90 | | `jenkins-jack.job.tree.numBuilds` | Number of builds to retrieve in the Job Tree view (NOTE: values over **100** will utilize the `allBuilds` field in the query, which may slow performance on the Jenkins server) | 91 | | `jenkins-jack.outputView.panel.defaultViewColumn` | The default view column (location) in vscode the output panel will spawn on show. See https://code.visualstudio.com/api/references/vscode-api#ViewColumn | 92 | | `jenkins-jack.outputView.suppressPipelineLog` | If enabled, hides `[Pipeline]` log lines in streamed output. | 93 | | `jenkins-jack.outputView.type` | The output view for streamed logs | 94 | | `jenkins-jack.pipeline.browserBuildOutput` | Show build output via browser instead of the `OUTPUT` channel | 95 | | `jenkins-jack.pipeline.browserSharedLibraryRef` | Show Pipeline Shared Library documentation via browser instead of within vscode as markdown | 96 | | `jenkins-jack.pipeline.params.enabled` | Enables the use of parameters (stored in '.myfile.config.json') to be used in your Pipeline execution | 97 | | `jenkins-jack.pipeline.params.interactiveInput` | If true, will grab parameters from the remote jenkins job and prompt user for builder parameter input using input boxes and quick picks. | 98 | | `jenkins-jack.pipeline.tree.items` | Remote jenkins job to local pipeline script associations | 99 | | `jenkins-jack.snippets.enabled` | Enable Pipeline step snippets for supported languageIds | 100 | | `jenkins-jack.tree.directorySeparator` | Directory separator string for job names in the Jenkins Jack TreeViews (default is `/`) | 101 | 102 | 103 | ## Setup 104 | 105 | See [TUTORIAL.md](TUTORIAL.md##setting-up-a-connection) for setup and basic usage. 106 | 107 | ## Quick-use 108 | 109 | ### `ctrl+shift+j` 110 | 111 | Displays a list of all Jack commands provided by the extension (`extension.jenkins-jack.jacks`) 112 | 113 | ## Local Packaging and Installation 114 | To create a standalone `vsix` for installation locally, run the following commands: 115 | ```bash 116 | # From the root of the extension. 117 | npm install -g vsce # For packaging 118 | npm install # Install dependencies. 119 | vsce package # Bake some bread. 120 | code --install-extension .\jenkins-jack-0.0.1.vsix # ...or whatever version was built 121 | ``` 122 | 123 | ## Contributing 124 | Do you have a feature request or would like to report a bug? Are you super motivated and want to submit a change? Do you think you're better than me? Most excellent! 125 | 126 | Please see the [contribution guide](CONTRIBUTING.md) for more deets. 127 | 128 | ## Authors 129 | 130 | * **Travis Abeyti** (*initial work*) 131 | 132 | ## License 133 | 134 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. Do what you will with this. 135 | -------------------------------------------------------------------------------- /src/jobJack.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { JackBase } from './jack'; 3 | import { JobTreeItem, JobTreeItemType } from './jobTree'; 4 | import { ext } from './extensionVariables'; 5 | import { JobType } from './jobType'; 6 | import { SelectionFlows } from './selectionFlows'; 7 | 8 | export class JobJack extends JackBase { 9 | 10 | constructor() { 11 | super('Job Jack', 'extension.jenkins-jack.job'); 12 | 13 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.job.delete', async (item?: any[] | JobTreeItem, items?: JobTreeItem[]) => { 14 | let result: boolean | undefined = false; 15 | if (item instanceof JobTreeItem) { 16 | let jobs = !items ? [item.job] : items.filter((item: JobTreeItem) => JobTreeItemType.Job === item.type).map((item: any) => item.job); 17 | result = await this.delete(jobs); 18 | } 19 | else { 20 | result = await this.delete(item); 21 | } 22 | 23 | if (result) { 24 | ext.jobTree.refresh(); 25 | ext.pipelineTree.refresh(); 26 | } 27 | })); 28 | 29 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.job.enable', async (item?: any[] | JobTreeItem, items?: JobTreeItem[]) => { 30 | let result: boolean | undefined = false; 31 | if (item instanceof JobTreeItem) { 32 | let jobs = !items ? [item.job] : items.filter((item: JobTreeItem) => JobTreeItemType.Job === item.type).map((item: any) => item.job); 33 | result = await this.enable(jobs); 34 | } 35 | else { 36 | result = await this.enable(item); 37 | } 38 | if (result) { ext.jobTree.refresh(); } 39 | })); 40 | 41 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.job.disable', async (item?: any[] | JobTreeItem, items?: any[]) => { 42 | let result: boolean | undefined = false; 43 | if (item instanceof JobTreeItem) { 44 | let jobs = !items ? [item.job] : items.filter((item: JobTreeItem) => JobTreeItemType.Job === item.type).map((item: any) => item.job); 45 | result = await this.disable(jobs); 46 | } 47 | else { 48 | await this.disable(item); 49 | } 50 | if (result) { ext.jobTree.refresh(); } 51 | })); 52 | 53 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.job.open', async (item?: any | JobTreeItem, items?: JobTreeItem[]) => { 54 | let jobs: any[] | undefined = []; 55 | if (item instanceof JobTreeItem) { 56 | jobs = items ? items.filter((item: JobTreeItem) => JobTreeItemType.Job === item.type).map((i: any) => i.job) : [item.job]; 57 | } 58 | else { 59 | jobs = await SelectionFlows.jobs(undefined, true); 60 | if (undefined === jobs) { return false; } 61 | } 62 | for (let job of jobs) { 63 | ext.connectionsManager.host.openBrowserAt(job.url); 64 | } 65 | })); 66 | } 67 | 68 | public get commands(): any[] { 69 | return [ 70 | { 71 | label: "$(stop) Job: Disable", 72 | description: "Disables targeted jobs from the remote Jenkins.", 73 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.job.disable') 74 | }, 75 | { 76 | label: "$(check) Job: Enable", 77 | description: "Enables targeted jobs from the remote Jenkins.", 78 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.job.enable') 79 | }, 80 | { 81 | label: "$(circle-slash) Job: Delete", 82 | description: "Deletes targeted jobs from the remote Jenkins.", 83 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.job.delete') 84 | }, 85 | { 86 | label: "$(browser) Job: Open", 87 | description: "Opens the targeted jobs in the user's browser.", 88 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.job.open') 89 | } 90 | ]; 91 | } 92 | 93 | public async enable(jobs?: any[]) { 94 | jobs = jobs ? jobs : await SelectionFlows.jobs((j: any) => !j.buildable && j.type !== JobType.Folder, true); 95 | if (undefined === jobs) { return; } 96 | return await this.actionOnJobs(jobs, async (job: any) => { 97 | await ext.connectionsManager.host.client.job.enable(job.fullName); 98 | return `"${job.fullName}" has been re-enabled`; 99 | }); 100 | } 101 | 102 | public async disable(jobs?: any[]) { 103 | jobs = jobs ? jobs : await SelectionFlows.jobs((j: any) => j.buildable && j.type !== JobType.Folder, true); 104 | if (undefined === jobs) { return; } 105 | return await this.actionOnJobs(jobs, async (job: any) => { 106 | await ext.connectionsManager.host.client.job.disable(job.fullName); 107 | return `"${job.fullName}" has been disabled`; 108 | }); 109 | } 110 | 111 | public async delete(jobs?: any[]) { 112 | jobs = jobs ? jobs : await SelectionFlows.jobs((j: any) => j.type !== JobType.Folder, true); 113 | if (undefined === jobs) { return; } 114 | 115 | let jobNames = jobs.map((j: any) => j.fullName); 116 | let r = await this.showInformationModal( 117 | `Are you sure you want to delete these jobs?\n\n${jobNames.join('\n')}`, 118 | { title: "Yes" } ); 119 | if (undefined === r) { return; } 120 | 121 | return await this.actionOnJobs(jobs, async (job: any) => { 122 | await ext.connectionsManager.host.client.job.destroy(job.fullName); 123 | return `"${job.fullName}" has been deleted`; 124 | }); 125 | } 126 | 127 | /** 128 | * Handles the flow for executing an action a list of jenkins job JSON objects. 129 | * @param jobs A list of jenkins job JSON objects. 130 | * label and returns output. 131 | * @param onJobAction The action to perform on the jobs. 132 | */ 133 | private async actionOnJobs( 134 | jobs: any[], 135 | onJobAction: (job: string) => Promise) { 136 | return vscode.window.withProgress({ 137 | location: vscode.ProgressLocation.Notification, 138 | title: `Job Jack Output(s)`, 139 | cancellable: true 140 | }, async (progress, token) => { 141 | 142 | token.onCancellationRequested(() => { 143 | this.showWarningMessage("User canceled job command."); 144 | }); 145 | 146 | let tasks = []; 147 | progress.report({ increment: 50, message: "Running command against Jenkins host..." }); 148 | for (let j of jobs) { 149 | let promise = new Promise(async (resolve) => { 150 | try { 151 | let output = await onJobAction(j); 152 | return resolve({ label: j.fullName, output: output }); 153 | } catch (err) { 154 | return resolve({ label: j.fullName, output: err }); 155 | } 156 | }); 157 | tasks.push(promise); 158 | } 159 | let results = await Promise.all(tasks); 160 | 161 | this.outputChannel.clear(); 162 | this.outputChannel.show(); 163 | for (let r of results as any[]) { 164 | this.outputChannel.appendLine(this.barrierLine); 165 | this.outputChannel.appendLine(r.label); 166 | this.outputChannel.appendLine(''); 167 | this.outputChannel.appendLine(r.output); 168 | this.outputChannel.appendLine(this.barrierLine); 169 | } 170 | progress.report({ increment: 50, message: `Output retrieved. Displaying in OUTPUT channel...` }); 171 | return true; 172 | }); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/nodeJack.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { JackBase } from './jack'; 3 | import { NodeTreeItem } from './nodeTree'; 4 | import { updateNodeLabelsScript } from './utils'; 5 | import { ext } from './extensionVariables'; 6 | import { SelectionFlows } from './selectionFlows'; 7 | 8 | export class NodeJack extends JackBase { 9 | 10 | constructor() { 11 | super('Node Jack', 'extension.jenkins-jack.node'); 12 | 13 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.node.setOffline', async (item?: any[] | NodeTreeItem, items?: NodeTreeItem[]) => { 14 | let result: any; 15 | if (item instanceof NodeTreeItem) { 16 | let nodes = !items ? [item.node] : items.map((item: any) => item.node); 17 | result = await this.setOffline(nodes); 18 | } 19 | else { 20 | result = await this.setOffline(item); 21 | } 22 | if (result) { ext.nodeTree.refresh(); } 23 | })); 24 | 25 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.node.setOnline', async (item?: any[] | NodeTreeItem, items?: NodeTreeItem[]) => { 26 | let result: any; 27 | if (item instanceof NodeTreeItem) { 28 | let nodes = !items ? [item.node] : items.map((item: any) => item.node); 29 | result = await this.setOnline(nodes); 30 | } 31 | else { 32 | result = await this.setOnline(item); 33 | } 34 | if (result) { ext.nodeTree.refresh(); } 35 | })); 36 | 37 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.node.disconnect', async (item?: any[] | NodeTreeItem, items?: NodeTreeItem[]) => { 38 | let result: any; 39 | if (item instanceof NodeTreeItem) { 40 | let nodes = !items ? [item.node] : items.map((item: any) => item.node); 41 | result = await this.disconnect(nodes); 42 | } 43 | else { 44 | result = await this.disconnect(item); 45 | } 46 | if (result) { ext.nodeTree.refresh(); } 47 | })); 48 | 49 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.node.updateLabels', async (item?: any[] | NodeTreeItem, items?: NodeTreeItem[]) => { 50 | let result: any; 51 | if (item instanceof NodeTreeItem) { 52 | let nodes = !items ? [item.node] : items.map((item: any) => item.node); 53 | result = await this.updateLabels(nodes); 54 | } 55 | else { 56 | result = await this.updateLabels(); 57 | } 58 | if (result) { ext.nodeTree.refresh(); } 59 | })); 60 | 61 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.node.open', async (item?: any | NodeTreeItem, items?: NodeTreeItem[]) => { 62 | let nodes = []; 63 | if (item instanceof NodeTreeItem) { 64 | nodes = items ? items.map((i: any) => i.node) : [item.node]; 65 | } 66 | else { 67 | nodes = await SelectionFlows.nodes(undefined, true, 'Select one or more nodes for opening in the browser'); 68 | if (undefined === nodes) { return false; } 69 | } 70 | for (let n of nodes) { 71 | ext.connectionsManager.host.openBrowserAtPath(`/computer/${n.displayName}`); 72 | } 73 | })); 74 | } 75 | 76 | public get commands(): any[] { 77 | return [ 78 | { 79 | label: "$(stop) Node: Set Offline", 80 | description: "Mark targeted nodes offline with a message.", 81 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.node.setOffline') 82 | }, 83 | { 84 | label: "$(check) Node: Set Online", 85 | description: "Mark targeted nodes online.", 86 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.node.setOnline') 87 | }, 88 | { 89 | label: "$(circle-slash) Node: Disconnect", 90 | description: "Disconnects targeted nodes from the host.", 91 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.node.disconnect') 92 | }, 93 | { 94 | label: "$(list-flat) Node: Update Labels", 95 | description: "Update targeted nodes' assigned labels.", 96 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.node.updateLabels') 97 | }, 98 | { 99 | label: "$(browser) Node: Open", 100 | description: "Opens the targeted nodes in the user's browser.", 101 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.node.open') 102 | } 103 | ]; 104 | } 105 | 106 | /** 107 | * Allows the user to select multiple offline nodes to be 108 | * re-enabled. 109 | */ 110 | public async setOnline(nodes?: any[]) { 111 | nodes = nodes ? nodes : await SelectionFlows.nodes( 112 | (n: any) => n.displayName !== 'master' && n.offline, 113 | true, 114 | 'Select one or more offline nodes for re-enabling'); 115 | 116 | if (undefined === nodes) { return undefined; } 117 | 118 | return await this.actionOnNodes(nodes, async (node: any) => { 119 | await ext.connectionsManager.host.client.node.enable(node.displayName); 120 | return `Online command sent to "${node.displayName}"`; 121 | }); 122 | } 123 | 124 | /** 125 | * Allows the user to select multiple online nodes to 126 | * be set in a temporary offline status, with a message. 127 | */ 128 | public async setOffline(nodes?: any[], offlineMessage?: string) { 129 | if (!offlineMessage) { 130 | offlineMessage = await vscode.window.showInputBox({ prompt: 'Enter an offline message.' }); 131 | if (undefined === offlineMessage) { return undefined; } 132 | } 133 | 134 | nodes = nodes ? nodes : await SelectionFlows.nodes( 135 | (n: any) => n.displayName !== 'master' && !n.offline, 136 | true, 137 | 'Select one or more nodes for temporary offline'); 138 | 139 | if (undefined === nodes) { return undefined; } 140 | 141 | return await this.actionOnNodes(nodes, async (node: any) => { 142 | await ext.connectionsManager.host.client.node.disable(node.displayName, offlineMessage); 143 | return `Offline command sent to "${node.displayName}"`; 144 | }); 145 | } 146 | 147 | /** 148 | * Allows the user to select multiple nodes to be 149 | * disconnected from the server. 150 | */ 151 | public async disconnect(nodes?: any[]) { 152 | nodes = nodes ? nodes : await SelectionFlows.nodes( 153 | (n: any) => n.displayName !== 'master', 154 | true, 155 | 'Select one or more nodes for disconnect'); 156 | 157 | if (undefined === nodes) { return undefined; } 158 | 159 | return await this.actionOnNodes(nodes, async (node: any) => { 160 | await ext.connectionsManager.host.client.node.disconnect(node.displayName); 161 | return `Disconnect command sent to "${node.displayName}"`; 162 | }); 163 | } 164 | 165 | public async updateLabels(nodes?: any) { 166 | nodes = nodes ? nodes : await SelectionFlows.nodes( 167 | (n: any) => n.displayName !== 'master', 168 | true, 169 | 'Select one or more nodes for updating labels'); 170 | 171 | if (undefined === nodes) { return undefined; } 172 | 173 | // Pull the labels from the first node to use as a pre-filled value 174 | // for the input box. 175 | let node = nodes[0]; 176 | let labelList = node.assignedLabels.map((l: any) => l.name).filter( 177 | (l: string) => l.toUpperCase() !== node.displayName.toUpperCase() 178 | ); 179 | 180 | let labelString = await vscode.window.showInputBox({ 181 | prompt: 'Enter the labels you want assigned to the node.', 182 | value: labelList.join(' ') 183 | }); 184 | if (undefined === labelString) { return undefined; } 185 | 186 | let nodeNames = nodes.map((n: any) => n.displayName); 187 | 188 | let script = updateNodeLabelsScript(nodeNames, labelString.split(' ')); 189 | let result = await ext.connectionsManager.host.runConsoleScript(script, undefined); 190 | 191 | this.outputChannel.clear(); 192 | this.outputChannel.show(); 193 | this.outputChannel.appendLine(this.barrierLine); 194 | this.outputChannel.appendLine(`Nodes Updated: ${nodeNames.join(', ')}`); 195 | this.outputChannel.appendLine(`Script Output: ${result}`); 196 | this.outputChannel.appendLine(this.barrierLine); 197 | return true; 198 | } 199 | 200 | /** 201 | * Handles an input flow for performing and action on targeted nodes. 202 | * @param onNodeAction Async callback that runs an action on a node 203 | * label and returns output. 204 | * @param filter Optional filter on a jenkins API node. 205 | */ 206 | private async actionOnNodes( 207 | nodes: any[], 208 | onNodeAction: (node: string) => Promise): Promise { 209 | 210 | return vscode.window.withProgress({ 211 | location: vscode.ProgressLocation.Notification, 212 | title: `Node Jack Output(s)`, 213 | cancellable: true 214 | }, async (progress, token) => { 215 | token.onCancellationRequested(() => { 216 | this.showWarningMessage("User canceled node command."); 217 | }); 218 | 219 | // Builds a list of parallel actions across the list of targeted machines 220 | // and awaits across all. 221 | let tasks = []; 222 | progress.report({ increment: 50, message: "Executing on target machine(s)" }); 223 | for (let n of nodes) { 224 | let promise = new Promise(async (resolve) => { 225 | try { 226 | let output = await onNodeAction(n); 227 | return resolve({ node: n.displayName, output: output }); 228 | } catch (err) { 229 | return resolve({ node: n.displayName, output: err }); 230 | } 231 | }); 232 | tasks.push(promise); 233 | } 234 | let results = await Promise.all(tasks); 235 | 236 | // Iterate over the result list, printing the name of the 237 | // machine and it's output. 238 | this.outputChannel.clear(); 239 | this.outputChannel.show(); 240 | for (let r of results as any[]) { 241 | this.outputChannel.appendLine(this.barrierLine); 242 | this.outputChannel.appendLine(r.node); 243 | this.outputChannel.appendLine(''); 244 | this.outputChannel.appendLine(r.output); 245 | this.outputChannel.appendLine(this.barrierLine); 246 | } 247 | progress.report({ increment: 50, message: `Output retrieved. Displaying in OUTPUT channel...` }); 248 | return true; 249 | }); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import { ext } from './extensionVariables'; 4 | import * as path from 'path'; 5 | import { JenkinsConnection } from './jenkinsConnection'; 6 | 7 | /** 8 | * Static class holding API query properties for jobs, builds, and nodes. 9 | */ 10 | export class QueryProperties { 11 | public static readonly job = [ 12 | 'name', 13 | 'fullName', 14 | 'url', 15 | 'buildable', 16 | 'inQueue', 17 | 'description' 18 | ].join(','); 19 | 20 | public static readonly jobMinimal = [ 21 | 'name', 22 | 'fullName', 23 | 'url', 24 | 'description' 25 | ].join(','); 26 | 27 | public static readonly build = [ 28 | 'number', 29 | 'result', 30 | 'description', 31 | 'url', 32 | 'duration', 33 | 'timestamp', 34 | 'building' 35 | ].join(','); 36 | 37 | public static readonly node = [ 38 | 'assignedLabels[name]', 39 | 'description', 40 | 'displayName', 41 | 'executors[idle,currentExecutable[displayName,timestamp,url]]', 42 | 'idle', 43 | 'offline', 44 | 'offlineCause', 45 | 'offlineCauseReason', 46 | 'temporarilyOffline' 47 | ].join(','); 48 | } 49 | 50 | function _sleep(ms: number) { 51 | return new Promise((resolve) => setTimeout(resolve, ms)); 52 | } 53 | 54 | /** 55 | * Async sleep utility method. 56 | * @param ms Milliseconds to sleep. 57 | */ 58 | export async function sleep(ms: number) { 59 | await _sleep(ms); 60 | } 61 | 62 | export function getValidEditor() { 63 | let langIds = [ 64 | "groovy", 65 | "jenkinsfile", 66 | "java" 67 | ]; 68 | var editor = vscode.window.activeTextEditor; 69 | if (!editor || !langIds.includes(editor?.document.languageId)) { 70 | return undefined; 71 | } 72 | return editor; 73 | } 74 | 75 | export function timer() { 76 | let timeStart = new Date().getTime(); 77 | return { 78 | get seconds() { 79 | const seconds = Math.ceil((new Date().getTime() - timeStart) / 1000) + 's'; 80 | return seconds; 81 | }, 82 | get ms() { 83 | const ms = (new Date().getTime() - timeStart) + 'ms'; 84 | return ms; 85 | } 86 | }; 87 | } 88 | 89 | export async function withProgressOutput(title: string, func: () => Promise): Promise { 90 | return await vscode.window.withProgress({ 91 | location: vscode.ProgressLocation.Window, 92 | title: title, 93 | cancellable: true 94 | }, async (progress, token) => { 95 | token.onCancellationRequested(() => { 96 | vscode.window.showWarningMessage("User canceled command."); 97 | }); 98 | return await func(); 99 | }); 100 | } 101 | 102 | export async function withProgressOutputParallel(title: string, items: any[], func: (i: any) => Promise) { 103 | return await vscode.window.withProgress({ 104 | location: vscode.ProgressLocation.Window, 105 | title: title, 106 | cancellable: true 107 | }, async (progress, token) => { 108 | token.onCancellationRequested(() => { 109 | vscode.window.showWarningMessage("User canceled command."); 110 | }); 111 | let results = await parallelTasks(items, func); 112 | return results.join(`\n${'-'.repeat(80)}\n`); 113 | }); 114 | } 115 | 116 | export async function showQuicPick(items: any[]): Promise { 117 | let qp = vscode.window.createQuickPick(); 118 | qp.items = items; 119 | qp.title = ''; 120 | 121 | } 122 | 123 | export function filepath(...filenameParts: string[]): string { 124 | return ext.context.asAbsolutePath(path.join(...filenameParts)); 125 | } 126 | 127 | /** 128 | * Runs through logic that shores up backwards-compatibility issues found in settings.json. 129 | * NOTE: also for backwards compatibility for older host settings found in v0.0.* 130 | */ 131 | export async function applyBackwardsCompat() { 132 | let jenkinsConfig = vscode.workspace.getConfiguration('jenkins-jack.jenkins'); 133 | 134 | // Applies default host or the legacy host connection info to the 135 | // list of jenkins hosts. 136 | let conns: any[] = (0 === jenkinsConfig.connections.length) ? 137 | [ 138 | { 139 | "name": "default", 140 | "uri": undefined === jenkinsConfig.uri ? 'http://127.0.0.1:8080' : jenkinsConfig.uri, 141 | "username": undefined === jenkinsConfig.username ? null : jenkinsConfig.username, 142 | "active": true 143 | } 144 | ] : 145 | jenkinsConfig.connections; 146 | 147 | 148 | // If any existing connections in settings.json have the "password" field, ask the user 149 | // if they would like to store them in the local-keystore for each connection. 150 | let connectionsWithPassword = jenkinsConfig.connections.filter((c: any) => null != c.password && '' != c.password); 151 | if (!connectionsWithPassword.length) { return; } 152 | 153 | let connectionsString = connectionsWithPassword.map((c: any) => c.name).join('\n'); 154 | 155 | let message = `Jenkins Jack: The latest version manages passwords from your system's key-store.\nWould you like to migrate your connection passwords in settings.json to the local key-store?\n\n${connectionsString}`; 156 | let result = await vscode.window.showInformationMessage(message, { modal: true }, { title: 'Yes' } ); 157 | if (undefined === result) { return undefined; } 158 | 159 | for (let c of connectionsWithPassword) { 160 | let conn = JenkinsConnection.fromJSON(c); 161 | await conn.setPassword(c.password); 162 | delete c.password; 163 | } 164 | vscode.window.showInformationMessage('Jenkins Jack: Passwords migrated successfully!'); 165 | 166 | await vscode.workspace.getConfiguration().update('jenkins-jack.jenkins.connections', conns, vscode.ConfigurationTarget.Global); 167 | } 168 | 169 | /** 170 | * Utility for parsing a json file and returning 171 | * its contents. 172 | * @param path The path to the json file. 173 | * @returns The parsed json. 174 | */ 175 | export function readjson(path: string): any { 176 | let raw: any = fs.readFileSync(path); 177 | let json: any; 178 | try { 179 | json = JSON.parse(raw); 180 | } catch (err) { 181 | err.message = `Could not parse parameter JSON from ${path}`; 182 | throw err; 183 | } 184 | return json; 185 | } 186 | 187 | /** 188 | * Writes the given json to disk. 189 | * @param path The the file path (file included) to write to. 190 | * @param json The json to write out. 191 | */ 192 | export function writejson(path: string, json: any) { 193 | try { 194 | let jsonString = JSON.stringify(json, null, 4); 195 | fs.writeFileSync(path, jsonString, 'utf8'); 196 | } catch (err) { 197 | err.message = `Could not write parameter JSON to ${path}`; 198 | throw err; 199 | } 200 | } 201 | 202 | /** 203 | * TODO: HACK 204 | * Returns some nasty hard-coded Jenkins Pipeline 205 | * XML as a Pipeline job config template. 206 | */ 207 | export function pipelineJobConfigXml() { 208 | return ` 209 | 210 | 211 | false 212 | 213 | 214 | false 215 | false 216 | 217 | 218 | 219 | 220 | false 221 | project 222 | false 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | false 232 | 233 | 234 | `; 235 | } 236 | 237 | export function addeNodeLabelsScript(nodes: string[], labels: string[]): string { 238 | 239 | let labelsToken = ''; 240 | let nodesToken = ''; 241 | for (let l of labels) { labelsToken += ` "${l}",`; } 242 | for (let n of nodes) { nodesToken += ` "${n}",`; } 243 | 244 | return `import jenkins.model.*; 245 | import jenkins.model.Jenkins; 246 | 247 | // Labels you want to add 248 | def additionalLabels = [ <> ]; 249 | 250 | // Target machines to update 251 | def nodeNames = [ <> ]; 252 | 253 | jenkins = Jenkins.instance; 254 | for (node in nodeNames) { 255 | println jenkins.getSlave(node); 256 | def node = jenkins.getNode(node); 257 | def labelsStr = node.labelString; 258 | 259 | validLabels = additionalLabels.findAll { l -> !labelsStr.contains(l) }; 260 | if (validLabels.isEmpty()) { 261 | continue; 262 | } 263 | def validLabels = validLabels.join(' '); 264 | jenkins.getNode(node).setLabelString(labelsStr + ' ' + validLabels); 265 | } 266 | 267 | jenkins.setNodes(jenkins.getNodes()); 268 | jenkins.save();`.replace('<>', labelsToken).replace('<>', nodesToken); 269 | } 270 | 271 | export async function parallelTasks(items: any, action: ((item: any) => Promise)): Promise { 272 | let tasks: Promise[] = []; 273 | for (let item of items) { 274 | let t = new Promise(async (resolve) => { 275 | return resolve(action(item)); 276 | }); 277 | tasks.push(t); 278 | } 279 | return await Promise.all(tasks); 280 | } 281 | 282 | /** 283 | * Groovy script for updating labels on the provided list of in nodes. 284 | * @param nodes A list of node names as strings 285 | * @param labels A list of labels to update on the nodes 286 | * @returns A script for updating nodes on the Jenkins server. 287 | */ 288 | export function updateNodeLabelsScript(nodes: string[], labels: string[]): string { 289 | let labelsToken = ''; 290 | let nodesToken = ''; 291 | for (let l of labels) { labelsToken += ` "${l}",`; } 292 | for (let n of nodes) { nodesToken += ` "${n}",`; } 293 | 294 | return `import jenkins.model.*; 295 | import jenkins.model.Jenkins; 296 | 297 | // Labels you want to add 298 | def newLabels = [ <> ]; 299 | 300 | // Target machines to update 301 | def nodeNames = [ <> ]; 302 | 303 | jenkins = Jenkins.instance; 304 | for (nodeName in nodeNames) { 305 | def node = jenkins.getNode(nodeName); 306 | def labelsStr = node.labelString; 307 | 308 | jenkins.getNode(nodeName).setLabelString(newLabels.join(' ')); 309 | } 310 | 311 | jenkins.setNodes(jenkins.getNodes()); 312 | jenkins.save();`.replace('<>', labelsToken).replace('<>', nodesToken); 313 | } 314 | 315 | /** 316 | * Converts a standard folder path into a supported Jenkins uri folder path. 317 | * @param folderPath The folder path (e.g. folder1/folder2/folder3) 318 | * @returns A supported Jenkins uri folder path (/folder1/job/folder2/job/folder3) 319 | */ 320 | export function folderToUri(folderPath: string) { 321 | return folderPath.split('/').join('/job/'); 322 | } 323 | 324 | /** 325 | * Converts timestamp into a date/time string. 326 | * @param timestamp Timestamp in milliseconds 327 | * @returns A formatted date/time string 328 | */ 329 | export function toDateString(timestamp: number): string { 330 | return `${new Date(timestamp).toLocaleString(undefined, { 331 | month: '2-digit', 332 | day: '2-digit', 333 | year: '2-digit', 334 | hour: '2-digit', 335 | minute: '2-digit', 336 | second: '2-digit', 337 | hour12: false})}` 338 | } 339 | 340 | /** 341 | * Converts milliseconds into HH:MM:SS.MS string. 342 | * Taken from: https://stackoverflow.com/a/19700358 343 | * @param duration Time in milliseconds 344 | */ 345 | export function msToTime(duration: number): string { 346 | // @ts-ignore 347 | var milliseconds = Math.floor((duration % 1000) / 100), 348 | seconds = Math.floor((duration / 1000) % 60), 349 | minutes = Math.floor((duration / (1000 * 60)) % 60), 350 | hours = Math.floor((duration / (1000 * 60 * 60)) % 24); 351 | 352 | let hrs = (hours < 10) ? "0" + hours : hours; 353 | let mins = (minutes < 10) ? "0" + minutes : minutes; 354 | let secs = (seconds < 10) ? "0" + seconds : seconds; 355 | 356 | return "+" + hrs + ":" + mins + ":" + secs; // + "." + milliseconds; 357 | } 358 | 359 | export function addDetail(detail: string): string { 360 | return `[${detail}] `; 361 | } 362 | -------------------------------------------------------------------------------- /src/buildJack.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { JackBase } from './jack'; 3 | import { JobTreeItem, JobTreeItemType } from './jobTree'; 4 | import { ext } from './extensionVariables'; 5 | import { withProgressOutputParallel } from './utils'; 6 | import { NodeTreeItem } from './nodeTree'; 7 | import { SelectionFlows } from './selectionFlows'; 8 | 9 | export class BuildJack extends JackBase { 10 | 11 | static JobBuild = class { 12 | public build: any; 13 | public job: any; 14 | }; 15 | 16 | constructor() { 17 | super('Build Jack', 'extension.jenkins-jack.build'); 18 | 19 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.build.abort', async (item?: any | JobTreeItem | NodeTreeItem, items?: any[]) => { 20 | 21 | if (item instanceof JobTreeItem) { 22 | items = !items ? [item] : items.filter((item: JobTreeItem) => JobTreeItemType.Build === item.type); 23 | } else if (item instanceof NodeTreeItem) { 24 | // HACKERY: For every NodeTreeItem "executor", parse the job name and build number 25 | // from the build "url" 26 | items = (items ?? [item]).map((i: any) => { 27 | return this.getJobBuildFromUrl(i.executor.currentExecutable.url); 28 | }); 29 | } else { 30 | let job = await SelectionFlows.jobs(undefined, false); 31 | if (undefined === job) { return; } 32 | 33 | let builds = await SelectionFlows.builds(job, (build: any) => build.building, true); 34 | if (undefined === builds) { return; } 35 | 36 | items = builds.map((b: any) => { return { job: job, build: b }; } ); 37 | } 38 | 39 | if (undefined === items) { return; } 40 | 41 | let buildNames = items.map((i: any) => `${i.job.fullName}: #${i.build.number}`); 42 | let r = await this.showInformationModal( 43 | `Are you sure you want to abort these builds?\n\n${buildNames.join('\n')}`, 44 | { title: "Yes"} ); 45 | if (undefined === r) { return undefined; } 46 | 47 | let output = await withProgressOutputParallel('Build Jack Output(s)', items, async (item) => { 48 | await ext.connectionsManager.host.client.build.stop(item.job.fullName, item.build.number); 49 | return `Abort signal sent to ${item.job.fullName}: #${item.build.number}`; 50 | }); 51 | this.outputChannel.clear(); 52 | this.outputChannel.show(); 53 | this.outputChannel.appendLine(output); 54 | ext.jobTree.refresh(); 55 | ext.nodeTree.refresh(2); 56 | })); 57 | 58 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.build.delete', async (item?: any | JobTreeItem, items?: JobTreeItem[]) => { 59 | if (item instanceof JobTreeItem) { 60 | items = !items ? [item] : items.filter((item: JobTreeItem) => JobTreeItemType.Build === item.type); 61 | } 62 | else { 63 | let job = await SelectionFlows.jobs(); 64 | if (undefined === job) { return; } 65 | 66 | let builds = await SelectionFlows.builds(job, undefined, true, 'Select a build'); 67 | if (undefined === builds) { return; } 68 | 69 | items = builds.map((b: any) => { return { job: job, build: b }; } ); 70 | } 71 | if (undefined === items) { return; } 72 | 73 | let buildNames = items.map((i: any) => `${i.job.fullName}: #${i.build.number}`); 74 | let r = await this.showInformationModal( 75 | `Are you sure you want to delete these builds?\n\n${buildNames.join('\n')}`, 76 | { title: "Yes"} ); 77 | if (undefined === r) { return undefined; } 78 | 79 | let output = await withProgressOutputParallel('Build Jack Output(s)', items, async (item) => { 80 | await ext.connectionsManager.host.deleteBuild(item.job, item.build.number); 81 | return `Deleted build ${item.job.fullName}: #${item.build.number}`; 82 | }); 83 | this.outputChannel.clear(); 84 | this.outputChannel.show(); 85 | this.outputChannel.appendLine(output); 86 | ext.jobTree.refresh(); 87 | })); 88 | 89 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.build.downloadLog', async (item?: any | JobTreeItem, items?: JobTreeItem[] | any[]) => { 90 | let targetItems: any[] = null; 91 | if (item instanceof JobTreeItem && null != items) { 92 | targetItems = items.filter((item: JobTreeItem) => JobTreeItemType.Build === item.type).map((i: any) => { 93 | return { job: i.job, build: i.build }; 94 | }); 95 | } 96 | else if (item instanceof JobTreeItem) { 97 | targetItems = [{ job: item.job, build: item.build }]; 98 | } 99 | else if (item instanceof NodeTreeItem) { 100 | // Filter only on non-idle executor tree items 101 | targetItems = !items ? [item] : items.filter((i: NodeTreeItem) => i.executor && !i.executor.idle); 102 | 103 | // HACK?: Because Jenkins queue api doesn't have a strong link to an executor's build, 104 | // we must extract the job/build information from the url. 105 | // @ts-ignore 106 | targetItems = targetItems.map((i: any) => { return this.getJobBuildFromUrl(i.executor.currentExecutable.url); }); 107 | } 108 | 109 | await this.downloadLog(targetItems); 110 | })); 111 | 112 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.build.downloadReplayScript', async (item?: any | JobTreeItem, items?: JobTreeItem[] | any[]) => { 113 | let targetItems: any[] = null; 114 | if (item instanceof JobTreeItem && null != items) { 115 | targetItems = items.filter((item: JobTreeItem) => JobTreeItemType.Build === item.type).map((i: any) => { 116 | return { job: i.job, build: i.build }; 117 | }); 118 | } 119 | else if (item instanceof JobTreeItem) { 120 | targetItems = [{ job: item.job, build: item.build }]; 121 | } 122 | else if (item instanceof NodeTreeItem) { 123 | // Filter only on non-idle executor tree items 124 | targetItems = !items ? [item] : items.filter((i: NodeTreeItem) => i.executor && !i.executor.idle); 125 | 126 | // HACK?: Because Jenkins queue api doesn't have a strong link to an executor's build, 127 | // we must extract the job/build information from the url. 128 | // @ts-ignore 129 | targetItems = targetItems.map((i: any) => { return this.getJobBuildFromUrl(i.executor.currentExecutable.url); }); 130 | } 131 | 132 | await this.downloadReplayScript(targetItems); 133 | })); 134 | 135 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.build.open', async (item?: any | JobTreeItem, items?: any[]) => { 136 | let urls = []; 137 | if (item instanceof JobTreeItem) { 138 | urls = items ? items.filter((item: JobTreeItem) => JobTreeItemType.Build === item.type).map((i: any) => i.build.url) : [item.build.url]; 139 | } 140 | else if (item instanceof NodeTreeItem) { 141 | urls = (items ?? [item]).map((i: any) => { return i.executor.currentExecutable.url }); 142 | } 143 | else { 144 | urls = (await SelectionFlows.builds(undefined, undefined, true))?.map((b: any) => b.url); 145 | if (undefined === urls) { return; } 146 | } 147 | 148 | for (let url of urls) { 149 | ext.connectionsManager.host.openBrowserAt(url); 150 | } 151 | })); 152 | } 153 | 154 | public get commands(): any[] { 155 | return [ 156 | { 157 | label: "$(stop) Build: Abort", 158 | description: "Select a job and builds to abort.", 159 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.build.abort') 160 | }, 161 | { 162 | label: "$(circle-slash) Build: Delete", 163 | description: "Select a job and builds to delete.", 164 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.build.delete') 165 | }, 166 | { 167 | label: "$(cloud-download) Build: Download Log", 168 | description: "Select a job and build to download the log.", 169 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.build.downloadLog') 170 | }, 171 | { 172 | label: "$(cloud-download) Build: Download Replay Script", 173 | description: "Pulls a pipeline replay script of a previous build into the editor.", 174 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.build.downloadReplayScript') 175 | }, 176 | { 177 | label: "$(browser) Build: Open", 178 | description: "Opens the targeted builds in the user's browser.", 179 | target: () => vscode.commands.executeCommand('extension.jenkins-jack.build.open') 180 | } 181 | ]; 182 | } 183 | 184 | /** 185 | * Downloads a build log for the user by first presenting a list 186 | * of jobs to select from, and then a list of build numbers for 187 | * the selected job. 188 | * @param job Optional job to target. If none, job selection will be presented. 189 | * @param builds Optional builds to target. If none, build selection will be presented. 190 | */ 191 | public async delete(job?: any, builds?: any[]) { 192 | job = job ? job : await SelectionFlows.jobs(); 193 | if (undefined === job) { return; } 194 | 195 | builds = builds ? builds : await SelectionFlows.builds(job, undefined, true); 196 | if (undefined === builds) { return; } 197 | 198 | let items = builds.map((b: any) => { return { job: job, build: b }; } ); 199 | 200 | let output = await withProgressOutputParallel('Build Jack Output(s)', items, async (item) => { 201 | await ext.connectionsManager.host.deleteBuild(item.job.fullName, item.build.number); 202 | return `Deleted build ${item.job.fullName}: #${item.build.number}`; 203 | }); 204 | this.outputChannel.clear(); 205 | this.outputChannel.show(); 206 | this.outputChannel.appendLine(output); 207 | } 208 | 209 | /** 210 | * Downloads a build log for the user by first presenting a list 211 | * of jobs to select from, and then a list of build numbers for 212 | * the selected job. 213 | * @param job Optional job to target. If none, job selection will be presented. 214 | * @param build Optional build to target. If none, build selection will be presented. 215 | */ 216 | public async downloadLog(items?: { job: any, build: any }[]) { 217 | if (!items) { 218 | let job = items ? items[0].job : await SelectionFlows.jobs(undefined, false); 219 | if (undefined === job) { return; } 220 | 221 | let builds = items ? items.map((i: any) => i.build) : await SelectionFlows.builds(job, null, true); 222 | if (undefined === builds) { return; } 223 | 224 | items = builds.map((b: any) => { return { job: job, build: b } } ); 225 | } 226 | 227 | // If this is a single item, use this instance's output channel to stream the build. 228 | if (1 === items.length) { 229 | ext.connectionsManager.host.streamBuildOutput(items[0].job.fullName, items[0].build.number, this.outputChannel); 230 | return 231 | } 232 | 233 | // If there are multiple items to download, create a new document for each build. 234 | for (let item of items) { 235 | let documentName = `${item.job.fullName.replaceAll('/', '-')}-${item.build.number}`; 236 | let outputPanel = ext.outputPanelProvider.get(documentName); 237 | 238 | // Stream it. Stream it until the editor crashes. 239 | ext.connectionsManager.host.streamBuildOutput(item.job.fullName, item.build.number, outputPanel); 240 | } 241 | } 242 | 243 | public async downloadReplayScript(items?: { job?: any, build?: any }[]) { 244 | if (!items) { 245 | let job = items ? items[0].job : await SelectionFlows.jobs(undefined, false); 246 | if (undefined === job) { return; } 247 | 248 | let builds = items ? items.map((i: any) => i.build) : await SelectionFlows.builds(job, null, true); 249 | if (undefined === builds) { return; } 250 | 251 | items = builds.map((b: any) => { return { job: job, build: b } } ); 252 | } 253 | 254 | await Promise.all(items.map(async (item: any) => { 255 | let script = await ext.connectionsManager.host.getReplayScript(item.job, item.build); 256 | if (undefined === script) { return; } 257 | let doc = await vscode.workspace.openTextDocument({ 258 | content: script, 259 | language: 'groovy' 260 | }); 261 | await vscode.window.showTextDocument(doc); 262 | })); 263 | } 264 | 265 | private getJobBuildFromUrl(url: string) { 266 | let jenkinsUri = ext.connectionsManager.host.connection.uri; 267 | url = url.replace(`${jenkinsUri}/`, ''); 268 | url = url.replace(/job\//g, ''); 269 | let urlParts = url.split('/').filter((c: string) => c !== '' ); 270 | return { 271 | job: { fullName: urlParts.slice(0, -1).join('/') }, 272 | build: { number: urlParts.slice(-1)[0] } 273 | }; 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/pipelineTree.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as vscode from 'vscode'; 3 | import * as path from 'path'; 4 | import { ext } from './extensionVariables'; 5 | import * as util from 'util'; 6 | import * as xml2js from "xml2js"; 7 | import { PipelineConfig } from './pipelineJobConfig'; 8 | import { JobType } from './jobType'; 9 | import { filepath } from './utils'; 10 | import { SelectionFlows } from './selectionFlows'; 11 | 12 | const parseXmlString = util.promisify(xml2js.parseString) as any as (xml: string) => any; 13 | 14 | export class PipelineTree { 15 | private readonly _treeView: vscode.TreeView; 16 | private readonly _treeViewDataProvider: PipelineTreeProvider; 17 | 18 | public constructor() { 19 | this._treeViewDataProvider = new PipelineTreeProvider(); 20 | this._treeView = vscode.window.createTreeView('pipelineTree', { treeDataProvider: this._treeViewDataProvider }); 21 | this._treeView.onDidChangeVisibility((e: vscode.TreeViewVisibilityChangeEvent) => { 22 | if (e.visible) { this.refresh(); } 23 | }); 24 | 25 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.pipeline.refresh', () => { 26 | this.refresh(); 27 | })); 28 | } 29 | 30 | public refresh() { 31 | // @ts-ignore 32 | this._treeView.title = `Pipelines (${ext.connectionsManager.host.connection.name})`; 33 | this._treeViewDataProvider.refresh(); 34 | } 35 | 36 | public get provider(): PipelineTreeProvider { 37 | return this._treeViewDataProvider; 38 | } 39 | } 40 | 41 | export class PipelineTreeProvider implements vscode.TreeDataProvider { 42 | private _pipelineTreeConfig: any; 43 | private _treeConfig: any; 44 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 45 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 46 | private _cancelTokenSource: vscode.CancellationTokenSource; 47 | 48 | public constructor() { 49 | this._cancelTokenSource = new vscode.CancellationTokenSource(); 50 | this.updateSettings(); 51 | vscode.workspace.onDidChangeConfiguration(event => { 52 | if (event.affectsConfiguration('jenkins-jack.pipeline.tree.items') || 53 | event.affectsConfiguration('jenkins-jack.tree')) { 54 | this.updateSettings(); 55 | } 56 | }); 57 | 58 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.pipeline.openScript', async (item: PipelineTreeItem) => { 59 | return await this.openScript(item); 60 | })); 61 | 62 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.pipeline.openScriptConfig', async (item: PipelineTreeItem) => { 63 | await this.openLocalScriptConfig(item); 64 | })); 65 | 66 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.pipeline.pullJobScript', async (item: PipelineTreeItem) => { 67 | await this.pullJobScript(item); 68 | })); 69 | 70 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.pipeline.pullReplayScript', async (item: PipelineTreeItem) => { 71 | await this.pullReplayScript(item); 72 | })); 73 | 74 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.pipeline.addLink', async (item: PipelineTreeItem) => { 75 | await this.addScriptLink(item); 76 | })); 77 | 78 | ext.context.subscriptions.push(vscode.commands.registerCommand('extension.jenkins-jack.tree.pipeline.removeLink', async (item: PipelineTreeItem) => { 79 | await this.deleteTreeItemConfig(item); 80 | })); 81 | } 82 | 83 | public async openScript(item: PipelineTreeItem) { 84 | let config = this.getTreeItemConfig(item.job.fullName); 85 | 86 | // If there is a mapping, but we can't find the file, ask to link to a local script 87 | if (null !== config.filepath && undefined !== config.filepath && !fs.existsSync(config.filepath)) { 88 | let r = await vscode.window.showInformationMessage( 89 | `"${config.filepath}" doesn't exist. Do you want to link to another script?`, { modal: true }, { title: "Yes"}); 90 | 91 | if (undefined === r) { return false; } 92 | } 93 | 94 | // If the script file path is not mapped, or we can't find the mapped script locally, 95 | // allow the user to select one locally. 96 | if (null === config.filepath || undefined === config.filepath || !fs.existsSync(config.filepath)) { 97 | let scriptResult = await vscode.window.showOpenDialog({ 98 | canSelectFiles: true, 99 | canSelectFolders: false, 100 | canSelectMany: false 101 | }); 102 | if (undefined === scriptResult) { return false; } 103 | 104 | // Update the tree item config with the new file path and save global config 105 | let scriptUri = scriptResult[0]; 106 | config.filepath = scriptUri.fsPath; 107 | 108 | await this.saveTreeItemsConfig(); 109 | } 110 | 111 | // Open the document in vscode 112 | let uri = vscode.Uri.parse(`file:${config.filepath}`); 113 | let editor = await vscode.window.showTextDocument(uri); 114 | await vscode.languages.setTextDocumentLanguage(editor.document, "groovy"); 115 | return true; 116 | } 117 | 118 | private updateSettings() { 119 | this._pipelineTreeConfig = vscode.workspace.getConfiguration('jenkins-jack.pipeline.tree'); 120 | this._treeConfig = vscode.workspace.getConfiguration('jenkins-jack.tree'); 121 | this.refresh(); 122 | } 123 | 124 | private async saveTreeItemsConfig() { 125 | await vscode.workspace.getConfiguration().update( 126 | 'jenkins-jack.pipeline.tree.items', 127 | this._pipelineTreeConfig.items.filter((i: any) => null !== i.filepath && undefined !== i.filepath), 128 | vscode.ConfigurationTarget.Global); 129 | this.refresh(); 130 | } 131 | 132 | private getTreeItemConfig(key: string): any { 133 | if (undefined === this._pipelineTreeConfig.items) { this._pipelineTreeConfig.items = []; } 134 | if (undefined === this._pipelineTreeConfig.items || undefined === this._pipelineTreeConfig.items.find( 135 | (i: any) => i.jobName === key && i.hostId === ext.connectionsManager.host.connection.name)) { 136 | this._pipelineTreeConfig.items.push({ 137 | hostId: ext.connectionsManager.host.connection.name, 138 | jobName: key, 139 | filepath: null, 140 | }); 141 | } 142 | return this._pipelineTreeConfig.items.find((i: any) => i.jobName === key && i.hostId === ext.connectionsManager.host.connection.name); 143 | } 144 | 145 | private async deleteTreeItemConfig(item: PipelineTreeItem) { 146 | await vscode.workspace.getConfiguration().update( 147 | 'jenkins-jack.pipeline.tree.items', 148 | this._pipelineTreeConfig.items.filter((i: any) => i.hostId !== ext.connectionsManager.host.connection.name || i.jobName !== item.job.fullName ), 149 | vscode.ConfigurationTarget.Global); 150 | } 151 | 152 | private async addScriptLink(item: PipelineTreeItem) { 153 | let jobName = path.parse(item.job.fullName).base; 154 | 155 | // Prompt user for folder location to save script 156 | let scriptFile = await vscode.window.showOpenDialog({ 157 | canSelectMany: false 158 | }); 159 | if (undefined === scriptFile) { return; } 160 | 161 | // Create local pipeline config for selected script 162 | let scriptFilePath = scriptFile[0].fsPath.replace(/\\/g, '/'); 163 | if (PipelineConfig.exists(scriptFilePath)) { 164 | let result = await vscode.window.showInformationMessage( 165 | `Pipeline config for ${scriptFilePath} already exists. Continuing will overwrite.`, 166 | { modal: true }, 167 | { title: 'Okay' }); 168 | if (undefined === result) { return; } 169 | } 170 | let pipelineJobConfig = new PipelineConfig(scriptFilePath, undefined, true); 171 | pipelineJobConfig.name = jobName; 172 | if (path.parse(item.job.fullName).dir !== '') { 173 | pipelineJobConfig.folder = path.dirname(item.job.fullName); 174 | } 175 | pipelineJobConfig.save(); 176 | 177 | // Link the script 178 | await this.linkScript(item.job.fullName, scriptFilePath); 179 | } 180 | 181 | public async linkScript(jobName: string, scriptFilePath: string) { 182 | // Update the filepath of this tree item's config, save it globally, and refresh tree items. 183 | this.getTreeItemConfig(jobName).filepath = scriptFilePath; 184 | await this.saveTreeItemsConfig(); 185 | } 186 | 187 | private async pullJobScript(item: PipelineTreeItem) { 188 | 189 | // See if script source exists on job 190 | let xml = await ext.connectionsManager.host.client.job.config(item.job.fullName).then((data: any) => { 191 | return data; 192 | }).catch((err: any) => { 193 | // TODO: Handle better 194 | ext.logger.error(err); 195 | throw err; 196 | }); 197 | 198 | let parsed = await parseXmlString(xml); 199 | let root = parsed['flow-definition']; 200 | let script = root.definition[0].script; 201 | if (undefined === script) { 202 | vscode.window.showInformationMessage(`Pipeline job "${item.label}" has no script to pull.`); 203 | return; 204 | } 205 | 206 | let pipelineConfig = await ext.pipelineJack.saveAndEditPipelineScript(script, item.job.fullName); 207 | if (undefined === pipelineConfig) { return; } 208 | await this.linkScript(item.job.fullName, pipelineConfig?.scriptPath); 209 | } 210 | 211 | private async pullReplayScript(item: PipelineTreeItem) { 212 | 213 | // Ask what build they want to download. 214 | let build = await SelectionFlows.builds(item.job); 215 | if (undefined === build) { return; } 216 | 217 | // Pull replay script from build number 218 | let script = await ext.connectionsManager.host.getReplayScript(item.job, build); 219 | if (undefined === script) { return; } 220 | 221 | let pipelineConfig = await ext.pipelineJack.saveAndEditPipelineScript(script, item.job.fullName); 222 | if (undefined === pipelineConfig) { return; } 223 | await this.linkScript(item.job.fullName, pipelineConfig?.scriptPath); 224 | } 225 | 226 | private async openLocalScriptConfig(item: PipelineTreeItem) { 227 | let pipelineConfig = new PipelineConfig(item.config.filepath); 228 | let uri = vscode.Uri.parse(`file:${pipelineConfig.path}`); 229 | let editor = await vscode.window.showTextDocument(uri); 230 | await vscode.languages.setTextDocumentLanguage(editor.document, "json"); 231 | } 232 | 233 | refresh(): void { 234 | this._cancelTokenSource.cancel(); 235 | this._cancelTokenSource.dispose(); 236 | this._cancelTokenSource = new vscode.CancellationTokenSource(); 237 | this._onDidChangeTreeData.fire(undefined); 238 | } 239 | 240 | getTreeItem(element: PipelineTreeItem): PipelineTreeItem { 241 | return element; 242 | } 243 | 244 | getChildren(element?: PipelineTreeItem): Thenable { 245 | return new Promise(async resolve => { 246 | let list = []; 247 | if (!ext.connectionsManager.connected) { 248 | resolve(list); 249 | return; 250 | } 251 | 252 | let jobs = await ext.connectionsManager.host.getJobs(null, { token: this._cancelTokenSource.token }); 253 | // Grab only pipeline jobs that are configurable/scriptable (no multi-branch, github org jobs) 254 | jobs = jobs.filter((job: any) => job._class === "org.jenkinsci.plugins.workflow.job.WorkflowJob" && 255 | job.buildable && 256 | job.type !== JobType.Multi && job.type !== JobType.Org 257 | ); 258 | 259 | for(let job of jobs) { 260 | let label = job.fullName.replace(/\//g, this._treeConfig.directorySeparator); 261 | let pipelineTreeItem = new PipelineTreeItem(label, job, this._pipelineTreeConfig.items.find((i: any) => i.jobName === job.fullName && i.hostId === ext.connectionsManager.host.connection.name)); 262 | // If there is an entry for this job tree item in the config, set the context of the tree item appropriately 263 | list.push(pipelineTreeItem); 264 | } 265 | resolve(list); 266 | }); 267 | } 268 | } 269 | 270 | export class PipelineTreeItem extends vscode.TreeItem { 271 | constructor( 272 | public readonly label: string, 273 | public readonly job: any, 274 | public readonly config: any 275 | ) { 276 | super(label, vscode.TreeItemCollapsibleState.None); 277 | 278 | let iconPrefix = (this.config) ? 'pipe-icon-linked' : 'pipe-icon-default'; 279 | this.contextValue = (this.config) ? 'pipeline-linked' : 'pipeline'; 280 | this.iconPath = { 281 | light: filepath('images', `${iconPrefix}-light.svg`), 282 | dark: filepath('images', `${iconPrefix}-dark.svg`) 283 | }; 284 | } 285 | 286 | // @ts-ignore 287 | get tooltip(): string { 288 | if (this.config) { 289 | return this.config.filepath; 290 | } 291 | 292 | if (undefined === this.job.description || '' === this.job.description) { 293 | return this.label; 294 | } 295 | else { 296 | return `${this.label} - ${this.job.description}`; 297 | } 298 | } 299 | 300 | // @ts-ignore 301 | get description(): string { 302 | return this.job.description; 303 | } 304 | 305 | contextValue = 'pipelineTreeItemDefault'; 306 | } 307 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the `jenkins-jack` extension will be documented in this file. 3 | 4 | ## 1.2.1 5 | 6 | ## Added 7 | * __Multi Download Support for Build Logs and Replay Scripts:__ Users can now select more than one build for download logs and replay scripts. When multiple items are selected, an individual document/tab will be created for each download. 8 | > __NOTE:__ Multi-item downloads will always open up multiple documents in the editor, regardless of the output view the user has defined for `jenkins-jack.outputView.type` in `settings.json`. 9 | 10 | ### Fixed 11 | * Broken `extension.jenkins-jack.build.open` command from the last update. 12 | * Fixed silent exception when item retrieval in `getChildren` for tree classes are undefined. 13 | * Removed `password` field from connection contribution point in `package.json`. 14 | 15 | ## 1.2.0 16 | 17 | Bumping minor version to accommodate a potentially breaking change to how connection passwords are stored. See __Changed__ section for details. 18 | 19 | ### Added 20 | * __Quick Folder Select for Active Connection:__ Users can now update the folder filter for the active connection on the fly without needing to edit the entire connection (`Connections: Select Folder`). 21 | * __Queue Jack and Queue TreeView:__ Allows the user to view items in the queue of the connected Jenkins. Users can also cancel one or more queued items (`Queue: Cancel`). 22 | * __Node Tree View Executors:__ The node tree now displays available executors for each node, along with active builds occupying these executors. This is similar to the agent/node pane on the left-hand side of a Jenkins' main page. Build operations can also be invoked on these items, such as `Abort` or `Download Log`. 23 | * __Jenkins Jack Log Output:__ Added a logger that writes to an output channel called `Jenkins Jack Log` to surface debug and exception information. 24 | 25 | ### Changed 26 | * __Password Stored on System Key-Store:__ To mitigate the security issue of connection passwords living in the `settings.json`, Jenkins Jack will now store a user's password for a connection/username under the local system's key-store using `keytar`: 27 | * New connections will automatically store passwords on the key-store. 28 | * Existing connections in `settings.json` with a `password` field will prompt the user on extension startup if they would like to migrate these passwords to the key-store and remove them from `settings.json`. 29 | > __NOTE:__ If a user declines to migrate the passwords, the extension will prompt the user to re-enter a password for the connection when attempting connect _if_ it is unable to locate it on the system's key-store 30 | * __Removed Auth Information from Connection URL:__ The connection URL used for requests will no longer include the username/password in URL format and now utilizes authentication headers like every other application on planet earth. Apologies for how long this took. 31 | 32 | ## 1.1.6 33 | 34 | ### Features 35 | * __Enable Build API Results Over 100 via `allBuilds`__: Any build operation will now prompt the user to enter a number of builds to retrieve (default 100). If over 100, the `allBuilds` field will be used within the query to pull more results. 36 | > __NOTE:__ Any value over 100, invoking the `allBuilds` filed, may have a performance impact on your Jenkins server due to it forcing the server (by default) to iterate over all builds. 37 | * __Job Tree Number of Build Results:__ A new setting `jenkins-jack.job.tree.numBuilds` is now available for configuring the number of build results to return for a job expansion (default 100). 38 | > __NOTE:__ Same performance impact as above applies. 39 | 40 | ## 1.1.5 41 | 42 | ### Fixes 43 | * __CSRF Protection Enabled for Connection Add Now Saving__: Fix for a bug where toggling the CSRF Protection Enabled while adding a connection wasn't being saved. 44 | 45 | ## 1.1.4 46 | 47 | ### Fixes 48 | * __CSRF Enabled Toggle for Connection Manager__: Added a toggle-able quick-pick for enabling and disabling CSRF protection when adding or editing a connection. This already existed but was somewhat hidden from the user and documentation wasn't really apparent in the TUTORIAL.md. 49 | >__NOTE__: This should only be disabled for older [Jenkins versions](https://www.jenkins.io/doc/book/security/csrf-protection/) pre 2.222. 50 | * __Connection Manager Password Field__: Password input box during connection add/edit is now set as "password input" so that characters aren't displayed when typing within the field. 51 | * __Node Tree Offline Reason in Description__: Offline reason now displayed in the tree item description (if any). 52 | 53 | ## 1.1.3 54 | 55 | ### Features 56 | * __Pipeline Jack: Create Job Command__: Added script/job/config create flow for scripted Pipeline jobs. This command will: 57 | * Prompt the user for a job name and a folder path (or root) to create the job under 58 | * Create the job on the Jenkins server 59 | * Prompt the user to save a local script 60 | * Auto-create the local pipeline config and link the local script for tree view commands 61 | * __Added Folder Selection on Pipeline Execute Job Creation ([#56](https://github.com/tabeyti/jenkins-jack/issues/56))__: Executing/creating pipelines under a Folder job on the Jenkins server isn't intuitive or user friendly. Now, on job creation, the user will be presented with a list of folders to select from to create their new pipeline under. 62 | * __Tree View Directory Separator Setting ([#47](https://github.com/tabeyti/jenkins-jack/issues/47))__: Users can specify what they want the directory separator string to be via the `jenkins-jack.tree.directorySeparator` setting. Default is `/`. 63 | 64 | ### Fixes 65 | * __Fixed Broken Open Browser Links ([#49](https://github.com/tabeyti/jenkins-jack/issues/49))__: Fixed usage of `Url.pathname` on a jenkins connection with a path (e.g. http://myhost/jenkins) which caused issues with url creation links for opening jobs/builds/nodes in the browser. 66 | * __Fixed Job/Folder Naming Issues with Pipeline Config__ ([#48](https://github.com/tabeyti/jenkins-jack/issues/48)): Fixed issue(s) around incorrect job name and folder paths being saved to a script's Pipeline json config. 67 | 68 | ## 1.1.2 69 | 70 | ### Fixes 71 | 72 | * __Fixed Broken Folder Jobs ([#43](https://github.com/tabeyti/jenkins-jack/issues/43))__: Folder jobs were broken for script linking and pipeline execute due to job name assignment. Folder pipeline jobs can now be executed and should display with the folder path in the tree view. 73 | * __Updated Folder Filter to Use Common Directory Path Structure__: The folder filter was introduce in the last update to allow a Jenkins connection to filter on a particular folder when querying jobs. 74 | The filter string format was originally to be a relative URI path to the folder (e.g. `job/myfolder/job/mysubfolder`). This messed with the Jenkins nodejs client on how jobs were queried. The filter now accepts a common directory path (e.g. `myfolder/mysubfolder`) for filtering, which requires the user to update their `folderFilter` to the new format. 75 | 76 | ## 1.1.1 77 | 78 | ### Features 79 | 80 | * __Filter Jobs by Folder ([#40](https://github.com/tabeyti/jenkins-jack/issues/40))__: Connection configs now have an (optional) feature to limit extension job queries to a specific folder in Jenkins: 81 | ```javascript 82 | "jenkins-jack.jenkins.connections": [ 83 | { 84 | "name": "localhost", 85 | "uri": "http://127.0.0.1:8080/jenkins", 86 | "username": "myusername", 87 | "password": "1231231231231231231231231231" 88 | // A relative URI folder path to the folder you want the extension to filter on 89 | "folderFilter": "job/myfolder" 90 | }, 91 | ] 92 | ``` 93 | 94 | If a user wanted filter on jobs that are in a sub-folder (nested folders)... 95 | ![Folder Filter Browser](images/doc/connection_folderfilter_browser.png) 96 | 97 | ...the folder filter path would be: 98 | ```javascript 99 | "folderFilter": "job/myfolder/job/subfolder" 100 | ``` 101 | 102 | * __Pipeline Log Line Suppression Setting ([#30](https://github.com/tabeyti/jenkins-jack/issues/30))__: A new setting/flag `jenkins-jack.outputView.suppressPipelineLog` is now available so that when set to `true`, will filter out all `[Pipeline]` log lines in user's output stream during Pipeline execution and build log download. 103 | 104 | * __Additional Language Support ([#20](https://github.com/tabeyti/jenkins-jack/issues/20))__: Additional language IDs (e.g. `jenkinsfile`, `java`) are supported as valid languages for Pipeline execution. 105 | 106 | ### Fixed 107 | * __Can't Execute/Update Jobs ([#38](https://github.com/tabeyti/jenkins-jack/issues/38))__: Fixed `forbidden` message appearing when users utilize non-api-token for authentication by setting `crumbIssuer: true` for Jenkins connections (CSRF protection). 108 | > __NOTE__: This is configurable for backwards compatibility against older Jenkins version (pre 2.222.1). If you are having connection problems after this change, the `crumbIssuer` can be disabled by modifying your specific Jenkins connection config located in `settings.json`: 109 | ```javascript 110 | "jenkins-jack.jenkins.connections": [ 111 | { 112 | "name": "localhost", 113 | "uri": "http://127.0.0.1:8080", 114 | "username": "myusername", 115 | "password": "1231231231231231231231231231" 116 | // Add/set "crumbIssuer" as "false" to disable CSRF protection 117 | "crumbIssuer": false 118 | }, 119 | ] 120 | ``` 121 | 122 | * __Username/Password URI Encoding Fix ([#41](https://github.com/tabeyti/jenkins-jack/issues/41))__: Both username and password fields are now URI encoded so that special characters are escaped in each connection field. 123 | 124 | ## 1.1.0 125 | 126 | Massive update adding UI/Views, additional commands, bug fixes, and additional documentation. 127 | 128 | ### Features 129 | 130 | #### Views 131 | 132 | The extensions now comes with UI/Views for interacting with all Jacks. The views can be found in the activity bar on the left hand side of the editor (bow icon): 133 | 134 | ![Views](images/doc/views.png) 135 | 136 | All commands a user can execute via the quickpick command list (`ctrl+shift+j`) can also be executed in the Views via context menu or buttons. 137 | 138 | For common use, see [TUTORIAL.md](TUTORIAL.md). 139 | 140 | #### Pipeline View 141 | 142 | This is a specialized view for managing your local Pipeline scripts in associations with Pipeline jobs discovered on the targeted Jenkins host. Linked jobs can be quickly opened and executed using the appropriate buttons. 143 | 144 | ![Views](images/doc/pipeline_view.png) 145 | 146 | This view also provides the ability to pull job/replay scripts for saving locally and linking to that job. 147 | 148 | Job to local script configuration can be found in `settings.json` under `jenkins-jack.pipeline.tree.items`. 149 | 150 | > **NOTE**: 151 | > The tree view (currently) will not pull __Multibranch__ or __Org__ level jobs. 152 | > For now the Pipeline Job Tree only works for standard Pipeline jobs. Yes, I am sad too. 153 | 154 | #### Pipeline Jack 155 | * __Folder Support__: Pipeline execution now supports Pipelines in folders! The folder path is entered/stored in the local script's json configuration under the `folder` property: 156 | ```javascript 157 | // For folder1/folder2/testjob 158 | // in config .testjob.config.json 159 | { 160 | "name": "testjob", 161 | "params": null, 162 | "folder": "folder1/folder2" 163 | } 164 | ``` 165 | * __Persist Jenkinsfile SCM configuration on Remote Job__: Executing a Pipeline script on a remote job with SCM information for using a Jenkinsfile will now silently save that SCM info and restore it after a build has started. Before the change, Pipeline execution would overwrite the job's config, embedding your script into the job and losing any SCM information that existed before. 166 | * __Interactive Build Parameter Input__: When enabled in the settings, during Pipeline execution, the user will be presented with input boxes for each build parameter on the remote job. When disabled, will act as before and utilize build parameter values from the local script's Pipeline config (e.g. `..config.json`). 167 | > __NOTE__: This option is disabled as default, but can be enabled via `settings.json` under `jenkins-jack.pipeline.params.interactiveInput`. 168 | 169 | #### Build Jack 170 | 171 | * __Download Replay Scripts__: New command to download build replay scripts to your editor from a targeted Pipeline job and build number. 172 | 173 | #### Node Jack 174 | 175 | * __Update Labels__: New command to update the label strings on one or more agents. 176 | 177 | ### Other 178 | 179 | * __Add/Delete/Edit Commands for Connections__: You can now manage your host connections through these new commands instead of modifying `settings.json` manually. Add/edit commands will present the user input boxes for the required connection fields. 180 | * __Update Docs__: New docs for use ([TUTORIAL.md](TUTORIAL.md)) and command reference ([COMMANDS.md](COMMANDS.md)) 181 | 182 | ### Fixed 183 | 184 | * Fixed "Cannot Connect" message appearing even after a user enters valid Jenkins connection info. 185 | * Fixed broken "jack" commands 186 | 187 | 188 | ## 1.0.1 189 | 190 | * __Stream Output to Editor Window__: All output can now be streamed to an editor window instead of the user's Output Channel. 191 | 192 | The output view type can be set in settings via `jenkins-jack.outputView.type` contribution point. 193 | 194 | The default location of the `panel` view type can be set via `jenkins-jack.outputView.panel.defaultViewColumn` contribution point. 195 | 196 | ### Fixed 197 | * Fixed issue where `Could not connect to the remote Jenkins` message would appear even after puting in correct connection information 198 | * Fixed command `extension.jenkins-jack.jacks` quick pick spacer icon 199 | 200 | ## 1.0.0 201 | 202 | First version. Yip skiddlee dooo! 203 | 204 | ## 0.1.6 205 | 206 | * New [logo](./images/logo.png) 207 | 208 | ### Fixed 209 | * Shared Library Reference now pulls definitions from any pipelines executed that include a shared lib (e.g. `@Library('shared')`). 210 | 211 | ## 0.1.6 212 | 213 | * __Build Jack:__ Build description now given when showing the list of build numbers to download. 214 | 215 | ### Fixed 216 | * Most "jacks" can now be invoked (`ctrl+shift+j`) without the need to be in a `groovy` file. Certain jack commands won't display if the view you are editing isn't set to the `groovy` language mode (e.g. Pipeline, Script Console) 217 | * Fixed progress window text formating. 218 | 219 | ## 0.1.5 220 | 221 | * __Job Jack:__ Execute disable, enable, and delete operations on one or more targeted jobs. 222 | * __Node Jack:__ Execute set-online, set-offline, and disconnect operations on one or more targeted nodes. 223 | * __Build Jack:__ Stream syntax higlighted build logs or delete one or more builds from a targeted job. 224 | 225 | ### Fixed 226 | * Default host connection now populates with default values properly 227 | * Fixed conditional logic for retrieving build numbers via jenkins url 228 | 229 | ## 0.1.4 230 | 231 | * __Multiple Host Connection Support:__ Now supports multiple Jenkins host connections and the ability to swap between hosts (`ctrl+shift+j -> Host Selection`) 232 | 233 | __NOTE:__ Additional hosts are added via `settings.json` which can be found in Settings by typing `Jenkins Jack`. 234 | 235 | * __Build Parameter Support for Pipeline Exection:__ Groovy files used for Pipeline execution now support parameters via a config file: `.conf.json`. Config file will be created automatically if one doesn't exist for a groovy file. 236 | 237 | * __Disabling Strict TLS:__ An option in Settings has been added to disable TLS checks for `https` enpoints that don't have a valid cert. 238 | 239 | * __Better Jenkins URI Parsing:__ Now supports prefixed (`http`/`https`) URIs. 240 | 241 | * __Progress Indicators Support Cancellation:__ Progress indicators now actually support canceling during pipeline execution, script console execution, or build log downloads. 242 | 243 | ### Fixed 244 | 245 | * __Snippets Refresh Fix__: When host information is changed, snippets will now update GDSL global shared library definitions correctly without a need for restarting the editor. 246 | 247 | ## 0.1.3 248 | 249 | ### Fixed 250 | - Broken `.pipeline` command in `packages.json` 251 | - Create job hang for Pipeline fixed; better error handling. 252 | 253 | ## 0.1.2 254 | 255 | ### Fixed 256 | 257 | - Snippets configuration now work 258 | 259 | ## 0.1.1 260 | - Initial release --------------------------------------------------------------------------------