├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .jscs.json ├── .npmignore ├── .prettierignore ├── .vscode └── launch.json ├── LICENSE ├── NOTES ├── README.md ├── lib ├── build.js ├── credentials.js ├── crumb_issuer.js ├── index.js ├── jenkins.js ├── job.js ├── label.js ├── log_stream.js ├── middleware.js ├── node.js ├── plugin.js ├── queue.js ├── utils.js └── view.js ├── package.json └── test ├── Dockerfile ├── compose.yml ├── fixtures ├── buildGet.json ├── consoleText.txt ├── credentialCreate.xml ├── credentialList.json ├── credentialUpdate.xml ├── folderCreate.xml ├── jobCreate.xml ├── jobGet.json ├── jobGetDisabled.json ├── jobList.json ├── jobUpdate.xml ├── labelGet.json ├── nodeConfigMaster.xml ├── nodeCreateQuery.txt ├── nodeGet.json ├── nodeGetOffline.json ├── nodeGetOfflineUpdate.json ├── nodeGetOnline.json ├── nodeGetTempOffline.json ├── nodeGetTempOfflineUpdate.json ├── nodeList.json ├── pluginList.json ├── queueItem.json ├── queueList.json ├── viewConfig.xml ├── viewCreate.json ├── viewGet.json ├── viewGetListView.json └── viewList.json ├── helper.js ├── jenkins.js ├── scriptApproval.xml ├── setup.groovy └── utils.js /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-22.04 14 | 15 | strategy: 16 | matrix: 17 | node-version: [16.x, 18.x, 20.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: npm install, build, and test 26 | run: | 27 | npm install 28 | npm run build --if-present 29 | npm test 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pid 3 | *swp 4 | .env 5 | .nyc_output 6 | config 7 | coverage 8 | hack.* 9 | node_modules 10 | package-lock.json 11 | tmp 12 | -------------------------------------------------------------------------------- /.jscs.json: -------------------------------------------------------------------------------- 1 | { 2 | "requireSpaceAfterKeywords": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "switch", 9 | "return", 10 | "try", 11 | "catch" 12 | ], 13 | "requireSpacesInFunctionExpression": { 14 | "beforeOpeningCurlyBrace": true 15 | }, 16 | "disallowSpacesInFunctionExpression": { 17 | "beforeOpeningRoundBrace": true 18 | }, 19 | "disallowEmptyBlocks": true, 20 | "requireSpacesInsideObjectBrackets": "all", 21 | "disallowSpacesInsideArrayBrackets": true, 22 | "disallowSpacesInsideParentheses": true, 23 | "disallowQuotedKeysInObjects": "allButReserved", 24 | "disallowSpaceAfterObjectKeys": true, 25 | "requireCommaBeforeLineBreak": true, 26 | "requireOperatorBeforeLineBreak": true, 27 | "requireSpaceAfterBinaryOperators": [ 28 | "?", 29 | "+", 30 | "/", 31 | "*", 32 | ":", 33 | "=", 34 | "==", 35 | "===", 36 | "!=", 37 | "!==", 38 | ">", 39 | ">=", 40 | "<", 41 | "<=" 42 | ], 43 | "requireSpacesInConditionalExpression": true, 44 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 45 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 46 | "requireSpaceBeforeBinaryOperators": [ 47 | "+", 48 | "-", 49 | "/", 50 | "*", 51 | "=", 52 | "==", 53 | "===", 54 | "!=", 55 | "!==" 56 | ], 57 | "requireSpaceAfterBinaryOperators": [ 58 | "+", 59 | "-", 60 | "/", 61 | "*", 62 | "=", 63 | "==", 64 | "===", 65 | "!=", 66 | "!==" 67 | ], 68 | "disallowKeywords": ["with"], 69 | "disallowMultipleLineStrings": true, 70 | "disallowMultipleLineBreaks": true, 71 | "validateLineBreaks": "LF", 72 | "validateQuoteMarks": true, 73 | "disallowTrailingWhitespace": true, 74 | "disallowKeywordsOnNewLine": ["else"], 75 | "requireCapitalizedConstructors": true, 76 | "safeContextKeyword": "self", 77 | "requireDotNotation": true, 78 | "excludeFiles": ["node_modules/**", "test/tmp/**"] 79 | } 80 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .env 2 | .istanbul.yml 3 | .jscs.json 4 | .jshintrc 5 | .npmignore 6 | config 7 | coverage 8 | docker-compose.yml 9 | test 10 | tmp 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Mocha Tests", 8 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 9 | "args": ["--recursive", "--check-leaks", "--timeout 15000"], 10 | "internalConsoleOptions": "openOnSessionStart", 11 | "skipFiles": ["/**"] 12 | }, 13 | { 14 | "type": "node", 15 | "request": "launch", 16 | "name": "Mocha No Nock", 17 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 18 | "args": ["--recursive", "--check-leaks", "--timeout 15000"], 19 | "internalConsoleOptions": "openOnSessionStart", 20 | "skipFiles": ["/**"], 21 | "env": { 22 | "NOCK_OFF": "true", 23 | "NOCK_REC": "false", 24 | "JENKINS_TEST_URL": "http://admin:admin@localhost:8080" 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Silas Sewell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /NOTES: -------------------------------------------------------------------------------- 1 | Software License Agreement (BSD License) 2 | 3 | Copyright (c) 2010, Willow Garage, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | * Neither the name of Willow Garage, Inc. nor the names of its 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | POSSIBILITY OF SUCH DAMAGE. 32 | 33 | Authors: 34 | Ken Conley 35 | James Page 36 | Tully Foote 37 | Matthew Gertner 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!CAUTION] 2 | > This project is no longer maintained. 3 | 4 | # Jenkins 5 | 6 | This is a Node.js client for [Jenkins](http://jenkins-ci.org/). 7 | 8 | ## Documentation 9 | 10 | - jenkins: [init](#init), [info](#info) 11 | - build: [get](#build-get), [log](#build-log), [logStream](#build-log-stream), [stop](#build-stop), [term](#build-term) 12 | - credentials: [create](#credentials-create), [exists](#credentials-exists), [get config](#credentials-get-config), [set config](#credentials-set-config), [destroy](#credentials-destroy), [list](#credentials-list) 13 | - job: [build](#job-build), [get config](#job-config-get), [set config](#job-config-set), [copy](#job-config-copy), [create](#job-create), [destroy](#job-destroy), [disable](#job-disable), [enable](#job-enable), [exists](#job-exists), [get](#job-get), [list](#job-list) 14 | - label: [get](#label-get) 15 | - node: [get config](#node-config-get), [create](#node-create), [destroy](#node-destroy), [disconnect](#node-disconnect), [disable](#node-disable), [enable](#node-enable), [exists](#node-exists), [get](#node-get), [list](#node-list) 16 | - plugin: [list](#plugin-list) 17 | - queue: [list](#queue-list), [item](#queue-item), [cancel](#queue-cancel) 18 | - view: [get config](#view-config-get), [set config](#view-config-set), [create](#view-create), [destroy](#view-destroy), [exists](#view-exists), [get](#view-get), [list](#view-list), [add job](#view-add), [remove job](#view-remove) 19 | 20 | 21 | 22 | ### Common Options 23 | 24 | These options will be passed along with any call, although only certain endpoints support them. 25 | 26 | - depth (Number, default: 0): how much data to return (see [depth control](https://wiki.jenkins-ci.org/display/JENKINS/Remote+access+API#RemoteaccessAPI-Depthcontrol)) 27 | - tree (String, optional): path expression (see Jenkins API documentation for more information) 28 | 29 | 30 | 31 | ### Jenkins(options) 32 | 33 | Initialize a new Jenkins client. 34 | 35 | Options 36 | 37 | - baseUrl (String): Jenkins URL 38 | - crumbIssuer (Boolean, default: true): enable CSRF Protection support 39 | - formData (Function, optional): enable file upload support on parameterized builds (must pass in `require('form-data')` as value for this option) 40 | - headers (Object, optional): headers included in every request 41 | - and more via [papi](https://github.com/silas/node-papi#client) 42 | 43 | Usage 44 | 45 | ```javascript 46 | import Jenkins from "jenkins"; 47 | 48 | const jenkins = new Jenkins({ 49 | baseUrl: "http://user:pass@localhost:8080", 50 | }); 51 | ``` 52 | 53 | 54 | 55 | ### jenkins.info(callback) 56 | 57 | Get server information. 58 | 59 | Usage 60 | 61 | ```javascript 62 | await jenkins.info(); 63 | ``` 64 | 65 | Result 66 | 67 | ```json 68 | { 69 | "assignedLabels": [{}], 70 | "description": null, 71 | "jobs": [ 72 | { 73 | "color": "blue", 74 | "name": "example", 75 | "url": "http://localhost:8080/job/example/" 76 | } 77 | ], 78 | "mode": "NORMAL", 79 | "nodeDescription": "the master Jenkins node", 80 | "nodeName": "", 81 | "numExecutors": 2, 82 | "overallLoad": {}, 83 | "primaryView": { 84 | "name": "All", 85 | "url": "http://localhost:8080/" 86 | }, 87 | "quietingDown": false, 88 | "slaveAgentPort": 12345, 89 | "unlabeledLoad": {}, 90 | "useCrumbs": false, 91 | "useSecurity": false, 92 | "views": [ 93 | { 94 | "name": "All", 95 | "url": "http://localhost:8080/" 96 | } 97 | ] 98 | } 99 | ``` 100 | 101 | 102 | 103 | ### jenkins.build.get(options) 104 | 105 | Get build information. 106 | 107 | Options 108 | 109 | - name (String): job name 110 | - number (Integer): build number 111 | 112 | Usage 113 | 114 | ```javascript 115 | await jenkins.build.get("example", 1); 116 | ``` 117 | 118 | Result 119 | 120 | ```json 121 | { 122 | "actions": [], 123 | "buildable": true, 124 | "builds": [ 125 | { 126 | "number": 1, 127 | "url": "http://localhost:8080/job/example/1/" 128 | } 129 | ], 130 | "color": "blue", 131 | "concurrentBuild": false, 132 | "description": "", 133 | "displayName": "example", 134 | "displayNameOrNull": null, 135 | "downstreamProjects": [], 136 | "firstBuild": { 137 | "number": 1, 138 | "url": "http://localhost:8080/job/example/1/" 139 | }, 140 | "healthReport": [ 141 | { 142 | "description": "Build stability: No recent builds failed.", 143 | "iconUrl": "health-80plus.png", 144 | "score": 100 145 | } 146 | ], 147 | "inQueue": false, 148 | "keepDependencies": false, 149 | "lastBuild": { 150 | "number": 1, 151 | "url": "http://localhost:8080/job/example/1/" 152 | }, 153 | "lastCompletedBuild": { 154 | "number": 1, 155 | "url": "http://localhost:8080/job/example/1/" 156 | }, 157 | "lastFailedBuild": null, 158 | "lastStableBuild": { 159 | "number": 1, 160 | "url": "http://localhost:8080/job/example/1/" 161 | }, 162 | "lastSuccessfulBuild": { 163 | "number": 1, 164 | "url": "http://localhost:8080/job/example/1/" 165 | }, 166 | "lastUnstableBuild": null, 167 | "lastUnsuccessfulBuild": null, 168 | "name": "example", 169 | "nextBuildNumber": 2, 170 | "property": [], 171 | "queueItem": null, 172 | "scm": {}, 173 | "upstreamProjects": [], 174 | "url": "http://localhost:8080/job/example/" 175 | } 176 | ``` 177 | 178 | 179 | 180 | ### jenkins.build.log(options) 181 | 182 | Get build log. 183 | 184 | Options 185 | 186 | - name (String): job name 187 | - number (Integer): build number 188 | - start (Integer, optional): start offset 189 | - type (String, enum: text, html, default: text): output format 190 | - meta (Boolean, default: false): return object with text (log data), more (boolean if there is more log data), and size (used with start to offset on subsequent calls) 191 | 192 | Usage 193 | 194 | ```javascript 195 | await jenkins.build.log("example", 1); 196 | ``` 197 | 198 | 199 | 200 | ### jenkins.build.logStream(options) 201 | 202 | Get build log stream. 203 | 204 | Options 205 | 206 | - name (String): job name 207 | - number (Integer): build number 208 | - type (String, enum: text, html, default: text): output format 209 | - delay (Integer, default: 1000): poll interval in milliseconds 210 | 211 | Usage 212 | 213 | ```javascript 214 | const log = jenkins.build.logStream("example", 1); 215 | 216 | log.on("data", (text) => { 217 | process.stdout.write(text); 218 | }); 219 | 220 | log.on("error", (err) => { 221 | console.log("error", err); 222 | }); 223 | 224 | log.on("end", () => { 225 | console.log("end"); 226 | }); 227 | ``` 228 | 229 | 230 | 231 | ### jenkins.build.stop(options) 232 | 233 | Stop build. 234 | 235 | Options 236 | 237 | - name (String): job name 238 | - number (Integer): build number 239 | 240 | Usage 241 | 242 | ```javascript 243 | await jenkins.build.stop("example", 1); 244 | ``` 245 | 246 | 247 | 248 | ### jenkins.build.term(options) 249 | 250 | Terminates build. 251 | 252 | Options 253 | 254 | - name (String): job name 255 | - number (Integer): build number 256 | 257 | Usage 258 | 259 | ```javascript 260 | await jenkins.build.term("example", 1); 261 | ``` 262 | 263 | 264 | 265 | ### jenkins.credentials.create(options) 266 | 267 | Create credentials in a folder or system. 268 | 269 | Options 270 | 271 | - folder (String): path of the folder or `manage` for **system** credentials 272 | - store (String): the credentials store, can be either `folder` or `system` 273 | - domain (String): the credentials domain 274 | - xml (String): configuration XML 275 | 276 | Usage 277 | 278 | ```javascript 279 | await jenkins.credentials.create("folder", "store", "domain", "xml"); 280 | ``` 281 | 282 | 283 | 284 | ### jenkins.credentials.exists(options) 285 | 286 | Check if credentials exist in a folder or system. 287 | 288 | Options 289 | 290 | - id (String): the id of the credentials 291 | - folder (String): path of the folder or `manage` for **system** credentials 292 | - store (String): the credentials store, can be either `folder` or `system` 293 | - domain (String): the credentials domain 294 | 295 | Usage 296 | 297 | ```javascript 298 | await jenkins.credentials.exists("id", "folder", "store", "domain"); 299 | ``` 300 | 301 | 302 | 303 | ### jenkins.credentials.config(options) 304 | 305 | Get XML configuration of credentials. 306 | 307 | Options 308 | 309 | - id (String): the id of the credentials 310 | - folder (String): path of the folder or `manage` for **system** credentials 311 | - store (String): the credentials store, can be either `folder` or `system` 312 | - domain (String): the credentials domain 313 | 314 | Usage 315 | 316 | ```javascript 317 | await jenkins.credentials.config("id", "folder", "store", "domain"); 318 | ``` 319 | 320 | 321 | 322 | ### jenkins.credentials.config(options) 323 | 324 | Update credentials. 325 | 326 | Options 327 | 328 | - id (String): the id of the credential 329 | - folder (String): path of the folder or `manage` for **system** credentials 330 | - store (String): the credentials store, can be either `folder` or `system` 331 | - domain (String): the credentials domain 332 | - xml (String): configuration XML 333 | 334 | Usage 335 | 336 | ```javascript 337 | await jenkins.credentials.update("id", "folder", "store", "domain", "xml"); 338 | ``` 339 | 340 | 341 | 342 | ### jenkins.credentials.destroy(options) 343 | 344 | Delete credentials from folder or system. 345 | 346 | Options 347 | 348 | - id (String): the id of the credential 349 | - folder (String): path of the folder or `manage` for **system** credentials 350 | - store (String): the credentials store, can be either `folder` or `system` 351 | - domain (String): the credentials domain 352 | 353 | Usage 354 | 355 | ```javascript 356 | await jenkins.credentials.destroy("id", "folder", "store", "domain"); 357 | ``` 358 | 359 | 360 | 361 | ### jenkins.credentials.list(options) 362 | 363 | Get a list of credentials in a folder or system. 364 | 365 | Options 366 | 367 | - folder (String): path of the folder or `manage` for **system** credentials 368 | - store (String): the credentials store, can be either `folder` or `system` 369 | - domain (String): the credentials domain 370 | 371 | Usage 372 | 373 | ```javascript 374 | await jenkins.credentials.list("folder", "store", "domain"); 375 | ``` 376 | 377 | 378 | 379 | ### jenkins.job.build(options) 380 | 381 | Trigger build. 382 | 383 | Options 384 | 385 | - name (String): job name 386 | - parameters (Object, optional): build parameters 387 | - token (String, optional): authorization token 388 | 389 | Usage 390 | 391 | ```javascript 392 | await jenkins.job.build("example"); 393 | ``` 394 | 395 | ```javascript 396 | await jenkins.job.build({ 397 | name: "example", 398 | parameters: { name: "value" }, 399 | }); 400 | ``` 401 | 402 | ```javascript 403 | await jenkins.job.build({ 404 | name: "example", 405 | parameters: { file: fs.createReadStream("test.txt") }, 406 | }); 407 | ``` 408 | 409 | 410 | 411 | ### jenkins.job.config(options) 412 | 413 | Get job XML configuration. 414 | 415 | Options 416 | 417 | - name (String): job name 418 | 419 | Usage 420 | 421 | ```javascript 422 | await jenkins.job.config("example"); 423 | ``` 424 | 425 | 426 | 427 | ### jenkins.job.config(options) 428 | 429 | Update job XML configuration. 430 | 431 | Options 432 | 433 | - name (String): job name 434 | - xml (String): configuration XML 435 | 436 | Usage 437 | 438 | ```javascript 439 | await jenkins.job.config("example", xml); 440 | ``` 441 | 442 | 443 | 444 | ### jenkins.job.copy(options) 445 | 446 | Create job by copying existing job. 447 | 448 | Options 449 | 450 | - name (String): new job name 451 | - from (String): source job name 452 | 453 | Usage 454 | 455 | ```javascript 456 | await jenkins.job.copy("fromJob", "example"); 457 | ``` 458 | 459 | 460 | 461 | ### jenkins.job.create(options) 462 | 463 | Create job from scratch. 464 | 465 | Options 466 | 467 | - name (String): job name 468 | - xml (String): configuration XML 469 | 470 | Usage 471 | 472 | ```javascript 473 | await jenkins.job.create("example", xml); 474 | ``` 475 | 476 | 477 | 478 | ### jenkins.job.destroy(options) 479 | 480 | Delete job. 481 | 482 | Options 483 | 484 | - name (String): job name 485 | 486 | Usage 487 | 488 | ```javascript 489 | await jenkins.job.destroy("example"); 490 | ``` 491 | 492 | 493 | 494 | ### jenkins.job.disable(options) 495 | 496 | Disable job. 497 | 498 | Options 499 | 500 | - name (String): job name 501 | 502 | Usage 503 | 504 | ```javascript 505 | await jenkins.job.disable("example"); 506 | ``` 507 | 508 | 509 | 510 | ### jenkins.job.enable(options) 511 | 512 | Enable job. 513 | 514 | Options 515 | 516 | - name (String): job name 517 | 518 | Usage 519 | 520 | ```javascript 521 | await jenkins.job.enable("example"); 522 | ``` 523 | 524 | 525 | 526 | ### jenkins.job.exists(options) 527 | 528 | Check job exists. 529 | 530 | Options 531 | 532 | - name (String): job name 533 | 534 | Usage 535 | 536 | ```javascript 537 | await jenkins.job.exists("example"); 538 | ``` 539 | 540 | 541 | 542 | ### jenkins.job.get(options) 543 | 544 | Get job information. 545 | 546 | Options 547 | 548 | - name (String): job name 549 | 550 | Usage 551 | 552 | ```javascript 553 | await jenkins.job.get("example"); 554 | ``` 555 | 556 | Result 557 | 558 | ```json 559 | { 560 | "actions": [], 561 | "buildable": true, 562 | "builds": [ 563 | { 564 | "number": 1, 565 | "url": "http://localhost:8080/job/example/1/" 566 | } 567 | ], 568 | "color": "blue", 569 | "concurrentBuild": false, 570 | "description": "", 571 | "displayName": "example", 572 | "displayNameOrNull": null, 573 | "downstreamProjects": [], 574 | "firstBuild": { 575 | "number": 1, 576 | "url": "http://localhost:8080/job/example/1/" 577 | }, 578 | "healthReport": [ 579 | { 580 | "description": "Build stability: No recent builds failed.", 581 | "iconUrl": "health-80plus.png", 582 | "score": 100 583 | } 584 | ], 585 | "inQueue": false, 586 | "keepDependencies": false, 587 | "lastBuild": { 588 | "number": 1, 589 | "url": "http://localhost:8080/job/example/1/" 590 | }, 591 | "lastCompletedBuild": { 592 | "number": 1, 593 | "url": "http://localhost:8080/job/example/1/" 594 | }, 595 | "lastFailedBuild": null, 596 | "lastStableBuild": { 597 | "number": 1, 598 | "url": "http://localhost:8080/job/example/1/" 599 | }, 600 | "lastSuccessfulBuild": { 601 | "number": 1, 602 | "url": "http://localhost:8080/job/example/1/" 603 | }, 604 | "lastUnstableBuild": null, 605 | "lastUnsuccessfulBuild": null, 606 | "name": "example", 607 | "nextBuildNumber": 2, 608 | "property": [], 609 | "queueItem": null, 610 | "scm": {}, 611 | "upstreamProjects": [], 612 | "url": "http://localhost:8080/job/example/" 613 | } 614 | ``` 615 | 616 | 617 | 618 | ### jenkins.job.list(callback) 619 | 620 | List jobs. 621 | 622 | Options 623 | 624 | - name (String, optional): folder name 625 | 626 | Usage 627 | 628 | ```javascript 629 | await jenkins.job.list(); 630 | ``` 631 | 632 | Result 633 | 634 | ```json 635 | [ 636 | { 637 | "color": "blue", 638 | "name": "example", 639 | "url": "http://localhost:8080/job/example/" 640 | } 641 | ] 642 | ``` 643 | 644 | 645 | 646 | ### jenkins.label.get(options) 647 | 648 | Get label information. 649 | 650 | Options 651 | 652 | - name (String): label name 653 | 654 | Usage 655 | 656 | ```javascript 657 | await jenkins.label.get("master"); 658 | ``` 659 | 660 | Result 661 | 662 | ```json 663 | { 664 | "_class": "hudson.model.labels.LabelAtom", 665 | "actions": [], 666 | "busyExecutors": 0, 667 | "clouds": [], 668 | "description": null, 669 | "idleExecutors": 2, 670 | "loadStatistics": { 671 | "_class": "hudson.model.Label$1" 672 | }, 673 | "name": "master", 674 | "nodes": [ 675 | { 676 | "_class": "hudson.model.Hudson", 677 | "nodeName": "" 678 | } 679 | ], 680 | "offline": false, 681 | "tiedJobs": [], 682 | "totalExecutors": 2, 683 | "propertiesList": [] 684 | } 685 | ``` 686 | 687 | 688 | 689 | ### jenkins.node.config(options) 690 | 691 | Get node XML configuration. 692 | 693 | Options 694 | 695 | - name (String): node name 696 | 697 | Usage 698 | 699 | ```javascript 700 | await jenkins.node.config("example"); 701 | ``` 702 | 703 | 704 | 705 | ### jenkins.node.create(options) 706 | 707 | Create node. 708 | 709 | Options 710 | 711 | - name (String): node name 712 | 713 | Usage 714 | 715 | ```javascript 716 | await jenkins.node.create("node-name"); 717 | ``` 718 | 719 | 720 | 721 | ### jenkins.node.destroy(options) 722 | 723 | Delete node. 724 | 725 | Options 726 | 727 | - name (String): node name 728 | 729 | Usage 730 | 731 | ```javascript 732 | await jenkins.node.destroy("node-name"); 733 | ``` 734 | 735 | 736 | 737 | ### jenkins.node.disconnect(options) 738 | 739 | Disconnect node. 740 | 741 | Options 742 | 743 | - name (String): node name 744 | - message (String, optional): reason for being disconnected 745 | 746 | Usage 747 | 748 | ```javascript 749 | await jenkins.node.disconnect("node-name", "no longer used"); 750 | ``` 751 | 752 | 753 | 754 | ### jenkins.node.disable(options) 755 | 756 | Disable node. 757 | 758 | Options 759 | 760 | - name (String): node name 761 | - message (String, optional): reason for being disabled 762 | 763 | Usage 764 | 765 | ```javascript 766 | await jenkins.node.disable("node-name", "network failure"); 767 | ``` 768 | 769 | 770 | 771 | ### jenkins.node.enable(options) 772 | 773 | Enable node. 774 | 775 | Options 776 | 777 | - name (String): node name 778 | 779 | Usage 780 | 781 | ```javascript 782 | await jenkins.node.enable("node-name"); 783 | ``` 784 | 785 | 786 | 787 | ### jenkins.node.exists(options) 788 | 789 | Check node exists. 790 | 791 | Options 792 | 793 | - name (String): node name 794 | 795 | Usage 796 | 797 | ```javascript 798 | await jenkins.node.exists("node-name"); 799 | ``` 800 | 801 | 802 | 803 | ### jenkins.node.get(options) 804 | 805 | Get node information. 806 | 807 | Options 808 | 809 | - name (String): node name 810 | 811 | Usage 812 | 813 | ```javascript 814 | await jenkins.node.get("node-name"); 815 | ``` 816 | 817 | Result 818 | 819 | ```json 820 | { 821 | "actions": [], 822 | "displayName": "node-name", 823 | "executors": [{}, {}], 824 | "icon": "computer-x.png", 825 | "idle": true, 826 | "jnlpAgent": true, 827 | "launchSupported": false, 828 | "loadStatistics": {}, 829 | "manualLaunchAllowed": true, 830 | "monitorData": { 831 | "hudson.node_monitors.ArchitectureMonitor": null, 832 | "hudson.node_monitors.ClockMonitor": null, 833 | "hudson.node_monitors.DiskSpaceMonitor": null, 834 | "hudson.node_monitors.ResponseTimeMonitor": { 835 | "average": 5000 836 | }, 837 | "hudson.node_monitors.SwapSpaceMonitor": null, 838 | "hudson.node_monitors.TemporarySpaceMonitor": null 839 | }, 840 | "numExecutors": 2, 841 | "offline": true, 842 | "offlineCause": null, 843 | "offlineCauseReason": "", 844 | "oneOffExecutors": [], 845 | "temporarilyOffline": false 846 | } 847 | ``` 848 | 849 | 850 | 851 | ### jenkins.node.list(callback) 852 | 853 | List all nodes. 854 | 855 | Options 856 | 857 | - full (Boolean, default: false): include executor count in response 858 | 859 | Usage 860 | 861 | ```javascript 862 | await jenkins.node.list(); 863 | ``` 864 | 865 | Result 866 | 867 | ```json 868 | { 869 | "busyExecutors": 0, 870 | "computer": [ 871 | { 872 | "actions": [], 873 | "displayName": "master", 874 | "executors": [{}, {}], 875 | "icon": "computer.png", 876 | "idle": true, 877 | "jnlpAgent": false, 878 | "launchSupported": true, 879 | "loadStatistics": {}, 880 | "manualLaunchAllowed": true, 881 | "monitorData": { 882 | "hudson.node_monitors.ArchitectureMonitor": "Linux (amd64)", 883 | "hudson.node_monitors.ClockMonitor": { 884 | "diff": 0 885 | }, 886 | "hudson.node_monitors.DiskSpaceMonitor": { 887 | "path": "/var/lib/jenkins", 888 | "size": 77620142080 889 | }, 890 | "hudson.node_monitors.ResponseTimeMonitor": { 891 | "average": 0 892 | }, 893 | "hudson.node_monitors.SwapSpaceMonitor": { 894 | "availablePhysicalMemory": 22761472, 895 | "availableSwapSpace": 794497024, 896 | "totalPhysicalMemory": 515358720, 897 | "totalSwapSpace": 805302272 898 | }, 899 | "hudson.node_monitors.TemporarySpaceMonitor": { 900 | "path": "/tmp", 901 | "size": 77620142080 902 | } 903 | }, 904 | "numExecutors": 2, 905 | "offline": false, 906 | "offlineCause": null, 907 | "offlineCauseReason": "", 908 | "oneOffExecutors": [], 909 | "temporarilyOffline": false 910 | }, 911 | { 912 | "actions": [], 913 | "displayName": "node-name", 914 | "executors": [{}, {}], 915 | "icon": "computer-x.png", 916 | "idle": true, 917 | "jnlpAgent": true, 918 | "launchSupported": false, 919 | "loadStatistics": {}, 920 | "manualLaunchAllowed": true, 921 | "monitorData": { 922 | "hudson.node_monitors.ArchitectureMonitor": null, 923 | "hudson.node_monitors.ClockMonitor": null, 924 | "hudson.node_monitors.DiskSpaceMonitor": null, 925 | "hudson.node_monitors.ResponseTimeMonitor": { 926 | "average": 5000 927 | }, 928 | "hudson.node_monitors.SwapSpaceMonitor": null, 929 | "hudson.node_monitors.TemporarySpaceMonitor": null 930 | }, 931 | "numExecutors": 2, 932 | "offline": true, 933 | "offlineCause": null, 934 | "offlineCauseReason": "", 935 | "oneOffExecutors": [], 936 | "temporarilyOffline": false 937 | } 938 | ], 939 | "displayName": "nodes", 940 | "totalExecutors": 2 941 | } 942 | ``` 943 | 944 | 945 | 946 | ### jenkins.plugin.list(callback) 947 | 948 | List plugins (note: depth defaults to 1). 949 | 950 | Usage 951 | 952 | ```javascript 953 | await jenkins.plugin.list(); 954 | ``` 955 | 956 | Result 957 | 958 | ```json 959 | [ 960 | { 961 | "active": true, 962 | "backupVersion": null, 963 | "bundled": false, 964 | "deleted": false, 965 | "dependencies": [{}, {}, {}, {}, {}, {}, {}, {}], 966 | "downgradable": false, 967 | "enabled": true, 968 | "hasUpdate": false, 969 | "longName": "Email Extension Plugin", 970 | "pinned": false, 971 | "shortName": "email-ext", 972 | "supportsDynamicLoad": "MAYBE", 973 | "url": "http://wiki.jenkins-ci.org/display/JENKINS/Email-ext+plugin", 974 | "version": "2.53" 975 | } 976 | ] 977 | ``` 978 | 979 | 980 | 981 | ### jenkins.queue.list(callback) 982 | 983 | List queues. 984 | 985 | Usage 986 | 987 | ```javascript 988 | await jenkins.queue.list(); 989 | ``` 990 | 991 | Result 992 | 993 | ```json 994 | { 995 | "items": [ 996 | { 997 | "actions": [ 998 | { 999 | "causes": [ 1000 | { 1001 | "shortDescription": "Started by user anonymous", 1002 | "userId": null, 1003 | "userName": "anonymous" 1004 | } 1005 | ] 1006 | } 1007 | ], 1008 | "blocked": true, 1009 | "buildable": false, 1010 | "buildableStartMilliseconds": 1389418977387, 1011 | "id": 20, 1012 | "inQueueSince": 1389418977358, 1013 | "params": "", 1014 | "stuck": false, 1015 | "task": { 1016 | "color": "blue_anime", 1017 | "name": "example", 1018 | "url": "http://localhost:8080/job/example/" 1019 | }, 1020 | "url": "queue/item/20/", 1021 | "why": "Build #2 is already in progress (ETA:N/A)" 1022 | } 1023 | ] 1024 | } 1025 | ``` 1026 | 1027 | 1028 | 1029 | ### jenkins.queue.item(options) 1030 | 1031 | Lookup a queue item. 1032 | 1033 | Options 1034 | 1035 | - number (Integer): queue item number 1036 | 1037 | Usage 1038 | 1039 | ```javascript 1040 | await jenkins.queue.item(130); 1041 | ``` 1042 | 1043 | Result 1044 | 1045 | ```json 1046 | { 1047 | "actions": [ 1048 | { 1049 | "causes": [ 1050 | { 1051 | "shortDescription": "Started by user anonymous", 1052 | "userId": null, 1053 | "userName": "anonymous" 1054 | } 1055 | ] 1056 | } 1057 | ], 1058 | "blocked": false, 1059 | "buildable": false, 1060 | "id": 130, 1061 | "inQueueSince": 1406363479853, 1062 | "params": "", 1063 | "stuck": false, 1064 | "task": { 1065 | "name": "test-job-b7ef0845-6515-444c-96a1-d2266d5e0f18", 1066 | "url": "http://localhost:8080/job/test-job-b7ef0845-6515-444c-96a1-d2266d5e0f18/", 1067 | "color": "blue" 1068 | }, 1069 | "url": "queue/item/130/", 1070 | "why": null, 1071 | "executable": { 1072 | "number": 28, 1073 | "url": "http://localhost:8080/job/test-job-b7ef0845-6515-444c-96a1-d2266d5e0f18/28/" 1074 | } 1075 | } 1076 | ``` 1077 | 1078 | 1079 | 1080 | ### jenkins.queue.cancel(options) 1081 | 1082 | Cancel build in queue. 1083 | 1084 | Options 1085 | 1086 | - number (Integer): queue item id 1087 | 1088 | Usage 1089 | 1090 | ```javascript 1091 | await jenkins.queue.cancel(23); 1092 | ``` 1093 | 1094 | 1095 | 1096 | ### jenkins.view.config(options) 1097 | 1098 | Get view XML configuration. 1099 | 1100 | Options 1101 | 1102 | - name (String): job name 1103 | 1104 | Usage 1105 | 1106 | ```javascript 1107 | await jenkins.view.config("example"); 1108 | ``` 1109 | 1110 | 1111 | 1112 | ### jenkins.view.config(options) 1113 | 1114 | Update view XML configuration. 1115 | 1116 | Options 1117 | 1118 | - name (String): job name 1119 | - xml (String): configuration XML 1120 | 1121 | Usage 1122 | 1123 | ```javascript 1124 | await jenkins.view.config("example", xml); 1125 | ``` 1126 | 1127 | 1128 | 1129 | ### jenkins.view.create(options) 1130 | 1131 | Create view. 1132 | 1133 | Options 1134 | 1135 | - name (String): view name 1136 | - type (String, enum: list, my): view type 1137 | 1138 | Usage 1139 | 1140 | ```javascript 1141 | await jenkins.view.create("example", "list"); 1142 | ``` 1143 | 1144 | 1145 | 1146 | ### jenkins.view.destroy(options) 1147 | 1148 | Delete view. 1149 | 1150 | Options 1151 | 1152 | - name (String): view name 1153 | 1154 | Usage 1155 | 1156 | ```javascript 1157 | await jenkins.view.destroy("example"); 1158 | ``` 1159 | 1160 | 1161 | 1162 | ### jenkins.view.exists(options) 1163 | 1164 | Check view exists. 1165 | 1166 | Options 1167 | 1168 | - name (String): view name 1169 | 1170 | Usage 1171 | 1172 | ```javascript 1173 | await jenkins.view.exists("example"); 1174 | ``` 1175 | 1176 | 1177 | 1178 | ### jenkins.view.get(options) 1179 | 1180 | Get view information. 1181 | 1182 | Options 1183 | 1184 | - name (String): view name 1185 | 1186 | Usage 1187 | 1188 | ```javascript 1189 | await jenkins.view.get("example"); 1190 | ``` 1191 | 1192 | Result 1193 | 1194 | ```json 1195 | { 1196 | "description": null, 1197 | "jobs": [ 1198 | { 1199 | "name": "test", 1200 | "url": "http://localhost:8080/job/example/", 1201 | "color": "blue" 1202 | } 1203 | ], 1204 | "name": "example", 1205 | "property": [], 1206 | "url": "http://localhost:8080/view/example/" 1207 | } 1208 | ``` 1209 | 1210 | 1211 | 1212 | ### jenkins.view.list(callback) 1213 | 1214 | List all views. 1215 | 1216 | Usage 1217 | 1218 | ```javascript 1219 | await jenkins.view.list(); 1220 | ``` 1221 | 1222 | Result 1223 | 1224 | ```json 1225 | { 1226 | "views": [ 1227 | { 1228 | "url": "http://localhost:8080/", 1229 | "name": "All" 1230 | }, 1231 | { 1232 | "url": "http://localhost:8080/view/example/", 1233 | "name": "Test" 1234 | } 1235 | ], 1236 | "useSecurity": false, 1237 | "useCrumbs": false, 1238 | "unlabeledLoad": {}, 1239 | "slaveAgentPort": 0, 1240 | "quietingDown": false, 1241 | "primaryView": { 1242 | "url": "http://localhost:8080/", 1243 | "name": "All" 1244 | }, 1245 | "assignedLabels": [{}], 1246 | "mode": "NORMAL", 1247 | "nodeDescription": "the master Jenkins node", 1248 | "nodeName": "", 1249 | "numExecutors": 2, 1250 | "description": null, 1251 | "jobs": [ 1252 | { 1253 | "color": "notbuilt", 1254 | "url": "http://localhost:8080/job/example/", 1255 | "name": "test" 1256 | } 1257 | ], 1258 | "overallLoad": {} 1259 | } 1260 | ``` 1261 | 1262 | 1263 | 1264 | ### jenkins.view.add(options) 1265 | 1266 | Add job to view. 1267 | 1268 | Options 1269 | 1270 | - name (String): view name 1271 | - job (String): job name 1272 | 1273 | Usage 1274 | 1275 | ```javascript 1276 | await jenkins.view.add("example", "jobExample"); 1277 | ``` 1278 | 1279 | 1280 | 1281 | ### jenkins.view.remove(options) 1282 | 1283 | Remove job from view. 1284 | 1285 | Options 1286 | 1287 | - name (String): view name 1288 | - job (String): job name 1289 | 1290 | Usage 1291 | 1292 | ```javascript 1293 | await jenkins.view.remove("example", "jobExample"); 1294 | ``` 1295 | 1296 | ## Test 1297 | 1298 | Run unit tests 1299 | 1300 | ```sh 1301 | $ npm test 1302 | ``` 1303 | 1304 | Run acceptance tests 1305 | 1306 | ```sh 1307 | $ docker compose -f test/compose.yml up -d --build 1308 | $ npm run acceptance 1309 | $ docker compose -f test/compose.yml down 1310 | ``` 1311 | 1312 | ## License 1313 | 1314 | This work is licensed under the MIT License (see the LICENSE file). 1315 | 1316 | ## Notes 1317 | 1318 | [python-jenkins](https://github.com/openstack/python-jenkins) (BSD License, see NOTES) 1319 | was used as a reference when implementing this client and its 1320 | create/reconfigure job XML was used in the tests. 1321 | -------------------------------------------------------------------------------- /lib/build.js: -------------------------------------------------------------------------------- 1 | const LogStream = require("./log_stream").LogStream; 2 | const middleware = require("./middleware"); 3 | const utils = require("./utils"); 4 | 5 | class Build { 6 | constructor(jenkins) { 7 | this.jenkins = jenkins; 8 | } 9 | 10 | /** 11 | * Build details 12 | */ 13 | async get(name, number, opts) { 14 | opts = utils.parse([...arguments], "name", "number"); 15 | 16 | this.jenkins._log(["debug", "build", "get"], opts); 17 | 18 | const req = { name: "build.get" }; 19 | 20 | utils.options(req, opts); 21 | 22 | try { 23 | const folder = utils.folderPath(opts.name); 24 | 25 | if (folder.isEmpty()) throw new Error("name required"); 26 | if (!opts.number) throw new Error("number required"); 27 | 28 | req.path = "{folder}/{number}/api/json"; 29 | req.params = { 30 | folder: folder.path(), 31 | number: opts.number, 32 | }; 33 | } catch (err) { 34 | throw this.jenkins._err(err, req); 35 | } 36 | 37 | return await this.jenkins._get( 38 | req, 39 | middleware.notFound(opts.name + " " + opts.number), 40 | middleware.body 41 | ); 42 | } 43 | 44 | /** 45 | * Stop build 46 | */ 47 | async stop(name, number, opts) { 48 | opts = utils.parse([...arguments], "name", "number"); 49 | 50 | this.jenkins._log(["debug", "build", "stop"], opts); 51 | 52 | const req = { name: "build.stop" }; 53 | 54 | utils.options(req, opts); 55 | 56 | try { 57 | const folder = utils.folderPath(opts.name); 58 | 59 | if (folder.isEmpty()) throw new Error("name required"); 60 | if (!opts.number) throw new Error("number required"); 61 | 62 | req.path = "{folder}/{number}/stop"; 63 | req.params = { 64 | folder: folder.path(), 65 | number: opts.number, 66 | }; 67 | } catch (err) { 68 | throw this.jenkins._err(err, req); 69 | } 70 | 71 | return await this.jenkins._post( 72 | req, 73 | middleware.notFound(opts.name + " " + opts.number), 74 | middleware.require302("failed to stop: " + opts.name), 75 | middleware.empty 76 | ); 77 | } 78 | 79 | /** 80 | * Terminate build 81 | */ 82 | async term(name, number, opts) { 83 | opts = utils.parse([...arguments], "name", "number"); 84 | 85 | this.jenkins._log(["debug", "build", "term"], opts); 86 | 87 | const req = { name: "build.term" }; 88 | 89 | utils.options(req, opts); 90 | 91 | try { 92 | const folder = utils.folderPath(opts.name); 93 | 94 | if (folder.isEmpty()) throw new Error("name required"); 95 | if (!opts.number) throw new Error("number required"); 96 | 97 | req.path = "{folder}/{number}/term"; 98 | req.params = { 99 | folder: folder.path(), 100 | number: opts.number, 101 | }; 102 | } catch (err) { 103 | throw this.jenkins._err(err, req); 104 | } 105 | 106 | return await this.jenkins._post( 107 | req, 108 | middleware.notFound(opts.name + " " + opts.number), 109 | middleware.empty 110 | ); 111 | } 112 | 113 | /** 114 | * Get build log 115 | */ 116 | async log(name, number, opts) { 117 | opts = utils.parse([...arguments], "name", "number"); 118 | 119 | this.jenkins._log(["debug", "build", "log"], opts); 120 | 121 | const req = { name: "build.log" }; 122 | 123 | utils.options(req, opts); 124 | 125 | try { 126 | const folder = utils.folderPath(opts.name); 127 | 128 | if (folder.isEmpty()) throw new Error("name required"); 129 | if (!opts.number) throw new Error("number required"); 130 | 131 | req.path = "{folder}/{number}/logText/progressive{type}"; 132 | req.params = { 133 | folder: folder.path(), 134 | number: opts.number, 135 | type: opts.type === "html" ? "Html" : "Text", 136 | }; 137 | req.type = "form"; 138 | req.body = {}; 139 | if (opts.hasOwnProperty("start")) req.body.start = opts.start; 140 | } catch (err) { 141 | throw this.jenkins._err(err, req); 142 | } 143 | 144 | return await this.jenkins._post( 145 | req, 146 | middleware.notFound(opts.name + " " + opts.number), 147 | (ctx, next) => { 148 | if (ctx.err) return next(ctx.err); 149 | if (!opts.meta) return next(false, ctx.res.body); 150 | 151 | const data = { 152 | text: ctx.res.body, 153 | more: ctx.res.headers["x-more-data"] === "true", 154 | }; 155 | 156 | if (ctx.res.headers["x-text-size"]) { 157 | data.size = ctx.res.headers["x-text-size"]; 158 | } 159 | 160 | next(false, data); 161 | } 162 | ); 163 | } 164 | 165 | /** 166 | * Get log stream 167 | */ 168 | logStream(name, number, opts) { 169 | opts = utils.parse([...arguments], "name", "number"); 170 | 171 | return new LogStream(this.jenkins, opts); 172 | } 173 | } 174 | 175 | exports.Build = Build; 176 | -------------------------------------------------------------------------------- /lib/credentials.js: -------------------------------------------------------------------------------- 1 | const middleware = require("./middleware"); 2 | const utils = require("./utils"); 3 | 4 | class Credentials { 5 | constructor(jenkins) { 6 | this.jenkins = jenkins; 7 | } 8 | 9 | /** 10 | * Get or update credentials 11 | */ 12 | async config(id, folderPath, store, domain, xml, opts) { 13 | opts = utils.parse( 14 | [...arguments], 15 | "id", 16 | "folder", 17 | "store", 18 | "domain", 19 | "xml" 20 | ); 21 | 22 | this.jenkins._log(["debug", "credentials", "config"], opts); 23 | 24 | const req = { name: "credentials.config" }; 25 | 26 | utils.options(req, opts); 27 | 28 | try { 29 | const folder = utils.folderPath(opts.folder); 30 | 31 | if (folder.isEmpty()) throw new Error("folder is required"); 32 | if (!opts.id) throw new Error("id is required"); 33 | if (!opts.store) throw new Error("store is required"); 34 | if (!opts.domain) throw new Error("domain is required"); 35 | 36 | req.path = 37 | "{folder}/credentials/store/{store}/domain/{domain}/credential/{id}/config.xml"; 38 | req.params = { 39 | folder: store === "system" ? folder.path("/") : folder.path(), 40 | store: opts.store, 41 | domain: opts.domain, 42 | id: opts.id, 43 | }; 44 | 45 | if (opts.xml) { 46 | req.method = "POST"; 47 | req.headers = { "content-type": "text/xml; charset=utf-8" }; 48 | req.body = Buffer.from(opts.xml); 49 | } else { 50 | req.method = "GET"; 51 | } 52 | } catch (err) { 53 | throw this.jenkins._err(err, req); 54 | } 55 | 56 | return await this.jenkins._request( 57 | req, 58 | middleware.notFound("job " + opts.folder), 59 | (ctx, next) => { 60 | if (ctx.err || opts.xml) return middleware.empty(ctx, next); 61 | 62 | next(false, ctx.res.body.toString("utf8")); 63 | } 64 | ); 65 | } 66 | 67 | /** 68 | * Create credentials 69 | */ 70 | async create(folder, store, domain, xml, opts) { 71 | opts = utils.parse([...arguments], "folder", "store", "domain", "xml"); 72 | 73 | this.jenkins._log(["debug", "credentials", "create"], opts); 74 | 75 | const req = { name: "credentials.create" }; 76 | 77 | utils.options(req, opts); 78 | 79 | try { 80 | const folder = utils.folderPath(opts.folder); 81 | 82 | if (folder.isEmpty()) throw new Error("folder required"); 83 | if (!opts.store) throw new Error("store required"); 84 | if (!opts.domain) throw new Error("domain required"); 85 | if (!opts.xml) throw new Error("xml required"); 86 | 87 | req.path = 88 | "{folder}/credentials/store/{store}/domain/{domain}/createCredentials"; 89 | req.headers = { "content-type": "text/xml; charset=utf-8" }; 90 | req.params = { 91 | folder: store === "system" ? folder.path("/") : folder.path(), 92 | store: opts.store, 93 | domain: opts.domain, 94 | id: opts.id, 95 | }; 96 | 97 | req.body = Buffer.from(opts.xml); 98 | } catch (err) { 99 | throw this.jenkins._err(err, req); 100 | } 101 | 102 | return await this.jenkins._post(req, middleware.empty); 103 | } 104 | 105 | /** 106 | * Check if credentials exist 107 | */ 108 | async exists(id, folderPath, store, domain, opts) { 109 | opts = utils.parse([...arguments], "id", "folder", "store", "domain"); 110 | 111 | this.jenkins._log(["debug", "credentials", "exists"], opts); 112 | 113 | const req = { name: "credentials.exists" }; 114 | 115 | utils.options(req, opts); 116 | 117 | try { 118 | const folder = utils.folderPath(opts.folder); 119 | 120 | if (folder.isEmpty()) throw new Error("folder required"); 121 | if (!opts.id) throw new Error("id required"); 122 | if (!opts.store) throw new Error("store required"); 123 | if (!opts.domain) throw new Error("domain required"); 124 | 125 | req.path = 126 | "{folder}/credentials/store/{store}/domain/{domain}/credential/{id}/api/json"; 127 | req.params = { 128 | folder: store === "system" ? folder.path("/") : folder.path(), 129 | store: opts.store, 130 | domain: opts.domain, 131 | id: opts.id, 132 | }; 133 | } catch (err) { 134 | throw this.jenkins._err(err, req); 135 | } 136 | 137 | return await this.jenkins._head(req, middleware.exists); 138 | } 139 | 140 | /** 141 | * Destroy credentials 142 | */ 143 | async destroy(id, folderPath, store, domain, opts) { 144 | opts = utils.parse([...arguments], "id", "folder", "store", "domain"); 145 | 146 | this.jenkins._log(["debug", "credentials", "destroy"], opts); 147 | 148 | const req = { name: "credentials.destroy" }; 149 | 150 | utils.options(req, opts); 151 | 152 | try { 153 | const folder = utils.folderPath(opts.folder); 154 | 155 | if (folder.isEmpty()) throw new Error("folder is required"); 156 | if (!opts.id) throw new Error("id is required"); 157 | if (!opts.store) throw new Error("store is required"); 158 | if (!opts.domain) throw new Error("domain is required"); 159 | 160 | req.path = 161 | "{folder}/credentials/store/{store}/domain/{domain}/credential/{id}/config.xml"; 162 | req.params = { 163 | folder: store === "system" ? folder.path("/") : folder.path(), 164 | store: opts.store, 165 | domain: opts.domain, 166 | id: opts.id, 167 | }; 168 | } catch (err) { 169 | throw this.jenkins._err(err, req); 170 | } 171 | 172 | return await this.jenkins._delete(req, middleware.empty); 173 | } 174 | 175 | /** 176 | * List credentials 177 | */ 178 | async list(folderPath, store, domain, opts) { 179 | opts = utils.parse([...arguments], "folder", "store", "domain"); 180 | 181 | this.jenkins._log(["debug", "credentials", "list"], opts); 182 | 183 | const req = { 184 | name: "credentials.list", 185 | }; 186 | 187 | utils.options(req, opts); 188 | 189 | try { 190 | const folder = utils.folderPath(opts.folder); 191 | 192 | if (!opts.domain) opts.domain = "_"; 193 | 194 | req.path = 195 | "{folder}/credentials/store/{store}/domain/{domain}/api/json?tree=credentials[id]"; 196 | req.params = { 197 | folder: store === "system" ? folder.path("/") : folder.path(), 198 | store: opts.store, 199 | domain: opts.domain, 200 | }; 201 | } catch (err) { 202 | throw this.jenkins._err(err, req); 203 | } 204 | 205 | return await this.jenkins._get( 206 | req, 207 | (ctx, next) => { 208 | if (ctx.err) return next(); 209 | 210 | if (ctx.err || opts.xml) return middleware.empty(ctx, next); 211 | 212 | if (!ctx.res.body || !Array.isArray(ctx.res.body.credentials)) { 213 | ctx.err = new Error("returned bad data"); 214 | } 215 | 216 | next(); 217 | }, 218 | middleware.bodyItem("credentials") 219 | ); 220 | } 221 | } 222 | 223 | exports.Credentials = Credentials; 224 | -------------------------------------------------------------------------------- /lib/crumb_issuer.js: -------------------------------------------------------------------------------- 1 | const utils = require("./utils"); 2 | 3 | class CrumbIssuer { 4 | constructor(jenkins) { 5 | this.jenkins = jenkins; 6 | } 7 | 8 | /** 9 | * Get crumb 10 | */ 11 | async get(opts) { 12 | opts = utils.parse([...arguments]); 13 | 14 | this.jenkins._log(["debug", "crumbIssuer", "get"], opts); 15 | 16 | const req = { 17 | name: "crumbIssuer.get", 18 | path: "/crumbIssuer/api/json", 19 | }; 20 | 21 | utils.options(req, opts); 22 | 23 | return await this.jenkins._get(req, (ctx, next) => { 24 | if (ctx.err) return next(ctx.err); 25 | 26 | const data = ctx.res.body; 27 | 28 | if (data && data._class === "hudson.security.csrf.DefaultCrumbIssuer") { 29 | const cookies = ctx.res.headers["set-cookie"]; 30 | 31 | if (cookies && cookies.length) { 32 | data.cookies = []; 33 | 34 | for (let i = 0; i < cookies.length; i++) { 35 | data.cookies.push(cookies[i].split(";")[0]); 36 | } 37 | } 38 | } 39 | 40 | next(false, data); 41 | }); 42 | } 43 | } 44 | 45 | exports.CrumbIssuer = CrumbIssuer; 46 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const Jenkins = require("./jenkins").Jenkins; 2 | 3 | module.exports = Jenkins; 4 | -------------------------------------------------------------------------------- /lib/jenkins.js: -------------------------------------------------------------------------------- 1 | const papi = require("papi"); 2 | const util = require("util"); 3 | 4 | const Build = require("./build").Build; 5 | const CrumbIssuer = require("./crumb_issuer").CrumbIssuer; 6 | const Job = require("./job").Job; 7 | const Label = require("./label").Label; 8 | const Node = require("./node").Node; 9 | const Plugin = require("./plugin").Plugin; 10 | const Queue = require("./queue").Queue; 11 | const View = require("./view").View; 12 | const Credentials = require("./credentials").Credentials; 13 | const middleware = require("./middleware"); 14 | const utils = require("./utils"); 15 | 16 | class Jenkins extends papi.Client { 17 | constructor(baseUrl, opts) { 18 | opts = utils.parse([...arguments], "baseUrl"); 19 | 20 | if (!opts.headers) { 21 | opts.headers = {}; 22 | } else { 23 | opts.headers = utils.clone(opts.headers); 24 | } 25 | if (!opts.headers.referer) { 26 | opts.headers.referer = opts.baseUrl + "/"; 27 | } 28 | 29 | opts.name = "jenkins"; 30 | 31 | const crumbIssuer = opts.crumbIssuer; 32 | const formData = opts.formData; 33 | 34 | delete opts.crumbIssuer; 35 | delete opts.formData; 36 | 37 | super(opts); 38 | 39 | if (typeof crumbIssuer === "function") { 40 | this._crumbIssuer = crumbIssuer; 41 | } else if (crumbIssuer === true || typeof crumbIssuer === "undefined") { 42 | this._crumbIssuer = utils.crumbIssuer; 43 | } 44 | 45 | if (formData) { 46 | if (typeof formData !== "function" || formData.name !== "FormData") { 47 | throw new Error("formData is invalid"); 48 | } 49 | this._formData = formData; 50 | } 51 | 52 | this._ext("onCreate", this._onCreate); 53 | this._ext("onResponse", this._onResponse); 54 | 55 | this.build = new Jenkins.Build(this); 56 | this.credentials = new Jenkins.Credentials(this); 57 | this.crumbIssuer = new Jenkins.CrumbIssuer(this); 58 | this.job = new Jenkins.Job(this); 59 | this.label = new Jenkins.Label(this); 60 | this.node = new Jenkins.Node(this); 61 | this.plugin = new Jenkins.Plugin(this); 62 | this.queue = new Jenkins.Queue(this); 63 | this.view = new Jenkins.View(this); 64 | } 65 | 66 | /** 67 | * Inject CSRF Protection crumb into POST requests 68 | */ 69 | _onCreate(ctx, next) { 70 | if (!this._crumbIssuer || ctx.opts.method !== "POST") return next(); 71 | 72 | this._crumbIssuer(this).then((data) => { 73 | if (data.headerName && data.headerValue) { 74 | if (!ctx.opts.headers) ctx.opts.headers = {}; 75 | ctx.opts.headers[data.headerName] = data.headerValue; 76 | if (data.cookies) ctx.opts.headers.cookie = data.cookies; 77 | } 78 | 79 | next(); 80 | }, next); 81 | } 82 | 83 | /** 84 | * Handle responses. 85 | */ 86 | _onResponse(ctx, next) { 87 | if (ctx.err) { 88 | if (ctx.res && ctx.res.headers && ctx.res.headers["x-error"]) { 89 | ctx.err.message = ctx.res.headers["x-error"].replace(/\?/g, '"'); 90 | } 91 | ctx.err.res = ctx.res; 92 | } 93 | 94 | next(); 95 | } 96 | 97 | /** 98 | * Jenkins info 99 | */ 100 | async info(opts) { 101 | opts = utils.parse([...arguments]); 102 | 103 | this._log(["debug", "info"], opts); 104 | 105 | const req = { 106 | name: "info", 107 | path: "/api/json", 108 | }; 109 | 110 | utils.options(req, opts); 111 | 112 | return this._get(req, middleware.body); 113 | } 114 | 115 | async get(...args) { 116 | return await info(...args); 117 | } 118 | } 119 | 120 | Jenkins.Build = Build; 121 | Jenkins.Credentials = Credentials; 122 | Jenkins.CrumbIssuer = CrumbIssuer; 123 | Jenkins.Job = Job; 124 | Jenkins.Label = Label; 125 | Jenkins.Node = Node; 126 | Jenkins.Plugin = Plugin; 127 | Jenkins.Queue = Queue; 128 | Jenkins.View = View; 129 | 130 | exports.Jenkins = Jenkins; 131 | -------------------------------------------------------------------------------- /lib/job.js: -------------------------------------------------------------------------------- 1 | const middleware = require("./middleware"); 2 | const utils = require("./utils"); 3 | 4 | class Job { 5 | constructor(jenkins) { 6 | this.jenkins = jenkins; 7 | } 8 | 9 | /** 10 | * Trigger job build 11 | */ 12 | async build(name, opts) { 13 | opts = utils.parse([...arguments], "name"); 14 | 15 | this.jenkins._log(["debug", "job", "build"], opts); 16 | 17 | const req = { name: "job.build" }; 18 | 19 | utils.options(req, opts); 20 | 21 | try { 22 | const folder = utils.folderPath(opts.name); 23 | 24 | if (folder.isEmpty()) throw new Error("name required"); 25 | 26 | req.path = "{folder}/build"; 27 | req.params = { folder: folder.path() }; 28 | 29 | if (typeof opts.parameters === "object") { 30 | req.path += "WithParameters"; 31 | 32 | let form; 33 | const data = {}; 34 | 35 | for (const [name, value] of Object.entries(opts.parameters)) { 36 | if (utils.isFileLike(value)) { 37 | if (!form) { 38 | if (!this.jenkins._formData) { 39 | throw new Error( 40 | "formData must be defined when client " + 41 | "initialized to use file upload" 42 | ); 43 | } 44 | form = new this.jenkins._formData(); 45 | } 46 | form.append(name, value, value.filename || name); 47 | } else { 48 | data[name] = value; 49 | } 50 | } 51 | 52 | if (form) { 53 | for (const [name, value] of Object.entries(data)) { 54 | form.append(name, value); 55 | } 56 | req.body = form; 57 | 58 | if (!req.headers) req.headers = {}; 59 | for (const [name, value] of Object.entries(form.getHeaders())) { 60 | req.headers[name] = value; 61 | } 62 | } 63 | 64 | if (!req.body) { 65 | req.type = "form"; 66 | req.body = data; 67 | } 68 | } 69 | 70 | if (opts.delay) req.query.delay = opts.delay; 71 | if (opts.token) req.query.token = opts.token; 72 | } catch (err) { 73 | throw this.jenkins._err(err, req); 74 | } 75 | 76 | return await this.jenkins._post( 77 | req, 78 | middleware.notFound(opts.name), 79 | middleware.ignoreErrorForStatusCodes(302), 80 | middleware.queueLocation 81 | ); 82 | } 83 | 84 | /** 85 | * Get or update config 86 | */ 87 | async config(name, xml, opts) { 88 | opts = utils.parse([...arguments], "name", "xml"); 89 | 90 | this.jenkins._log(["debug", "job", "config"], opts); 91 | 92 | const req = { name: "job.config" }; 93 | 94 | utils.options(req, opts); 95 | 96 | try { 97 | const folder = utils.folderPath(opts.name); 98 | 99 | if (folder.isEmpty()) throw new Error("name required"); 100 | 101 | req.path = "{folder}/config.xml"; 102 | req.params = { folder: folder.path() }; 103 | 104 | if (opts.xml) { 105 | req.method = "POST"; 106 | req.headers = { "content-type": "text/xml; charset=utf-8" }; 107 | req.body = Buffer.from(opts.xml); 108 | } else { 109 | req.method = "GET"; 110 | } 111 | } catch (err) { 112 | throw this.jenkins._err(err, req); 113 | } 114 | 115 | return await this.jenkins._request( 116 | req, 117 | middleware.notFound("job " + opts.name), 118 | (ctx, next) => { 119 | if (ctx.err || opts.xml) return middleware.empty(ctx, next); 120 | 121 | next(false, ctx.res.body.toString("utf8")); 122 | } 123 | ); 124 | } 125 | 126 | /** 127 | * Copy job 128 | */ 129 | async copy(from, name, opts) { 130 | opts = utils.parse([...arguments], "from", "name"); 131 | 132 | this.jenkins._log(["debug", "job", "copy"], opts); 133 | 134 | const req = { name: "job.copy" }; 135 | 136 | utils.options(req, opts); 137 | 138 | try { 139 | const folder = utils.folderPath(opts.name); 140 | 141 | if (folder.isEmpty()) throw new Error("name required"); 142 | if (!opts.from) throw new Error("from required"); 143 | 144 | req.path = "{dir}/createItem"; 145 | req.headers = { "content-type": "text/xml; charset=utf-8" }; 146 | req.params = { dir: folder.dir() }; 147 | req.query.name = folder.name(); 148 | req.query.from = opts.from; 149 | req.query.mode = "copy"; 150 | } catch (err) { 151 | throw this.jenkins._err(err, req); 152 | } 153 | 154 | return await this.jenkins._post( 155 | req, 156 | middleware.require302("failed to create: " + opts.name), 157 | middleware.empty 158 | ); 159 | } 160 | 161 | /** 162 | * Create new job from xml 163 | */ 164 | async create(name, xml, opts) { 165 | opts = utils.parse([...arguments], "name", "xml"); 166 | 167 | this.jenkins._log(["debug", "job", "create"], opts); 168 | 169 | const req = { name: "job.create" }; 170 | 171 | utils.options(req, opts); 172 | 173 | try { 174 | const folder = utils.folderPath(opts.name); 175 | 176 | if (folder.isEmpty()) throw new Error("name required"); 177 | if (!opts.xml) throw new Error("xml required"); 178 | 179 | req.path = "{dir}/createItem"; 180 | req.headers = { "content-type": "text/xml; charset=utf-8" }; 181 | req.params = { dir: folder.dir() }; 182 | req.query.name = folder.name(); 183 | req.body = Buffer.from(opts.xml); 184 | } catch (err) { 185 | throw this.jenkins._err(err, req); 186 | } 187 | 188 | return await this.jenkins._post(req, middleware.empty); 189 | } 190 | 191 | /** 192 | * Destroy job 193 | */ 194 | async destroy(name, opts) { 195 | opts = utils.parse([...arguments], "name"); 196 | 197 | this.jenkins._log(["debug", "job", "destroy"], opts); 198 | 199 | const req = { name: "job.destroy" }; 200 | 201 | utils.options(req, opts); 202 | 203 | try { 204 | const folder = utils.folderPath(opts.name); 205 | 206 | if (folder.isEmpty()) throw new Error("name required"); 207 | 208 | req.path = "{folder}/doDelete"; 209 | req.params = { folder: folder.path() }; 210 | } catch (err) { 211 | throw this.jenkins._err(err, req); 212 | } 213 | 214 | return await this.jenkins._post( 215 | req, 216 | middleware.notFound(opts.name), 217 | middleware.require302("failed to delete: " + opts.name), 218 | middleware.empty 219 | ); 220 | } 221 | 222 | async delete(...args) { 223 | return await this.destroy(...args); 224 | } 225 | 226 | /** 227 | * Disable job 228 | */ 229 | async disable(name, opts) { 230 | opts = utils.parse([...arguments], "name"); 231 | 232 | this.jenkins._log(["debug", "job", "disable"], opts); 233 | 234 | const req = { name: "job.disable" }; 235 | 236 | utils.options(req, opts); 237 | 238 | try { 239 | const folder = utils.folderPath(opts.name); 240 | 241 | if (folder.isEmpty()) throw new Error("name required"); 242 | 243 | req.path = "{folder}/disable"; 244 | req.params = { folder: folder.path() }; 245 | } catch (err) { 246 | throw this.jenkins._err(err, req); 247 | } 248 | 249 | return await this.jenkins._post( 250 | req, 251 | middleware.notFound(opts.name), 252 | middleware.require302("failed to disable: " + opts.name), 253 | middleware.empty 254 | ); 255 | } 256 | 257 | /** 258 | * Enable job 259 | */ 260 | async enable(name, opts) { 261 | opts = utils.parse([...arguments], "name"); 262 | 263 | this.jenkins._log(["debug", "job", "enable"], opts); 264 | 265 | const req = { name: "job.enable" }; 266 | 267 | utils.options(req, opts); 268 | 269 | try { 270 | const folder = utils.folderPath(opts.name); 271 | 272 | if (folder.isEmpty()) throw new Error("name required"); 273 | 274 | req.path = "{folder}/enable"; 275 | req.params = { folder: folder.path() }; 276 | } catch (err) { 277 | throw this.jenkins._err(err, req); 278 | } 279 | 280 | return await this.jenkins._post( 281 | req, 282 | middleware.notFound(opts.name), 283 | middleware.require302("failed to enable: " + opts.name), 284 | middleware.empty 285 | ); 286 | } 287 | 288 | /** 289 | * Job exists 290 | */ 291 | async exists(name, opts) { 292 | opts = utils.parse([...arguments], "name"); 293 | 294 | this.jenkins._log(["debug", "job", "exists"], opts); 295 | 296 | const req = { name: "job.exists" }; 297 | 298 | utils.options(req, opts); 299 | 300 | try { 301 | const folder = utils.folderPath(opts.name); 302 | 303 | if (folder.isEmpty()) throw new Error("name required"); 304 | 305 | req.path = "{folder}/api/json"; 306 | req.params = { folder: folder.path() }; 307 | } catch (err) { 308 | throw this.jenkins._err(err, req); 309 | } 310 | 311 | return await this.jenkins._head(req, middleware.exists); 312 | } 313 | 314 | /** 315 | * Job details 316 | */ 317 | async get(name, opts) { 318 | opts = utils.parse([...arguments], "name"); 319 | 320 | this.jenkins._log(["debug", "job", "get"], opts); 321 | 322 | const req = { name: "job.get" }; 323 | 324 | utils.options(req, opts); 325 | 326 | try { 327 | const folder = utils.folderPath(opts.name); 328 | 329 | if (folder.isEmpty()) throw new Error("name required"); 330 | 331 | req.path = "{folder}/api/json"; 332 | req.params = { folder: folder.path() }; 333 | } catch (err) { 334 | throw this.jenkins._err(err, req); 335 | } 336 | 337 | return await this.jenkins._get( 338 | req, 339 | middleware.notFound(opts.name), 340 | middleware.body 341 | ); 342 | } 343 | 344 | /** 345 | * List jobs 346 | */ 347 | async list(name, opts) { 348 | opts = utils.parse([...arguments], "name"); 349 | 350 | this.jenkins._log(["debug", "job", "list"], opts); 351 | 352 | const req = { 353 | name: "job.list", 354 | path: "/api/json", 355 | }; 356 | 357 | utils.options(req, opts); 358 | 359 | if (opts.name) { 360 | try { 361 | const folder = utils.folderPath(opts.name); 362 | 363 | if (folder.isEmpty()) throw new Error("name required"); 364 | 365 | req.path = "{folder}/api/json"; 366 | req.params = { folder: folder.path() }; 367 | } catch (err) { 368 | throw this.jenkins._err(err, req); 369 | } 370 | } 371 | 372 | return await this.jenkins._get( 373 | req, 374 | (ctx, next) => { 375 | if (ctx.err) return next(); 376 | 377 | if (!ctx.res.body || !Array.isArray(ctx.res.body.jobs)) { 378 | ctx.err = new Error("returned bad data"); 379 | } 380 | 381 | next(); 382 | }, 383 | middleware.bodyItem("jobs") 384 | ); 385 | } 386 | } 387 | 388 | exports.Job = Job; 389 | -------------------------------------------------------------------------------- /lib/label.js: -------------------------------------------------------------------------------- 1 | const middleware = require("./middleware"); 2 | const utils = require("./utils"); 3 | 4 | class Label { 5 | constructor(jenkins) { 6 | this.jenkins = jenkins; 7 | } 8 | 9 | /** 10 | * Label details 11 | */ 12 | async get(name, opts) { 13 | opts = utils.parse([...arguments], "name"); 14 | 15 | this.jenkins._log(["debug", "label", "get"], opts); 16 | 17 | const req = { name: "label.get" }; 18 | 19 | utils.options(req, opts); 20 | 21 | try { 22 | if (!opts.name) throw new Error("name required"); 23 | 24 | req.path = "/label/{name}/api/json"; 25 | req.params = { 26 | name: opts.name, 27 | }; 28 | } catch (err) { 29 | throw this.jenkins._err(err, req); 30 | } 31 | 32 | return await this.jenkins._get(req, middleware.body); 33 | } 34 | } 35 | 36 | exports.Label = Label; 37 | -------------------------------------------------------------------------------- /lib/log_stream.js: -------------------------------------------------------------------------------- 1 | const events = require("events"); 2 | 3 | const utils = require("./utils"); 4 | 5 | class LogStream extends events.EventEmitter { 6 | constructor(jenkins, opts) { 7 | super(); 8 | 9 | this._jenkins = jenkins; 10 | 11 | opts = utils.clone(opts || {}); 12 | 13 | this._delay = opts.delay || 1000; 14 | delete opts.delay; 15 | 16 | this._opts = opts; 17 | this._opts.meta = true; 18 | 19 | process.nextTick(() => { 20 | this._run(); 21 | }); 22 | } 23 | 24 | /** 25 | * End watch 26 | */ 27 | end() { 28 | clearTimeout(this._timeoutId); 29 | 30 | if (this._end) return; 31 | this._end = true; 32 | 33 | this.emit("end"); 34 | } 35 | 36 | /** 37 | * Error helper 38 | */ 39 | _err(err) { 40 | if (this._end) return; 41 | 42 | this.emit("error", err); 43 | 44 | this.end(); 45 | } 46 | 47 | /** 48 | * Run 49 | */ 50 | async _run() { 51 | if (this._end) return; 52 | 53 | try { 54 | const data = await this._jenkins.build.log(this._opts); 55 | if (this._end) return; 56 | 57 | if (typeof data.text === "string") this.emit("data", data.text); 58 | 59 | if (!data.more) return this.end(); 60 | if (data.size) this._opts.start = data.size; 61 | 62 | this._timeoutId = setTimeout(() => { 63 | this._run(); 64 | }, this._delay); 65 | } catch (err) { 66 | if (this._end) return; 67 | return this._err(err); 68 | } 69 | } 70 | } 71 | 72 | exports.LogStream = LogStream; 73 | -------------------------------------------------------------------------------- /lib/middleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Body 3 | */ 4 | function body(ctx, next) { 5 | if (ctx.err) return next(ctx.err); 6 | 7 | next(false, ctx.res.body); 8 | } 9 | 10 | /** 11 | * Body item 12 | */ 13 | function bodyItem(key) { 14 | return (ctx, next) => { 15 | if (ctx.err) return next(ctx.err); 16 | 17 | next(false, ctx.res.body[key]); 18 | }; 19 | } 20 | 21 | /** 22 | * Empty 23 | */ 24 | function empty(ctx, next) { 25 | if (ctx.err) return next(ctx.err); 26 | 27 | next(false); 28 | } 29 | 30 | /** 31 | * Exists 32 | */ 33 | function exists(ctx, next) { 34 | if (ctx.res && ctx.res.statusCode === 404) { 35 | return next(false, false); 36 | } 37 | 38 | if (ctx.err) return next(ctx.err); 39 | 40 | next(false, true); 41 | } 42 | 43 | /** 44 | * Ignore errors for provided status codes 45 | */ 46 | function ignoreErrorForStatusCodes() { 47 | const statusCodes = Array.prototype.slice.call(arguments); 48 | 49 | return (ctx, next) => { 50 | if (ctx.err && ctx.res && statusCodes.indexOf(ctx.res.statusCode) !== -1) { 51 | delete ctx.err; 52 | } 53 | 54 | next(); 55 | }; 56 | } 57 | 58 | /** 59 | * Require 302 or error 60 | */ 61 | function require302(message) { 62 | return (ctx, next) => { 63 | if (ctx.res && ctx.res.statusCode === 302) { 64 | return next(false); 65 | } else if (ctx.res) { 66 | if (ctx.err) { 67 | if (!ctx.res.headers["x-error"]) ctx.err.message = message; 68 | } else { 69 | ctx.err = new Error(message); 70 | } 71 | 72 | return next(ctx.err); 73 | } 74 | 75 | next(); 76 | }; 77 | } 78 | 79 | /** 80 | * Not found 81 | */ 82 | function notFound(value) { 83 | return (ctx, next) => { 84 | if (ctx.res && ctx.res.statusCode === 404) { 85 | const err = new Error(value + " not found"); 86 | err.notFound = true; 87 | 88 | return next(err); 89 | } 90 | 91 | next(); 92 | }; 93 | } 94 | 95 | /** 96 | * Queue location 97 | */ 98 | function queueLocation(ctx, next) { 99 | if (ctx.err) return next(ctx.err); 100 | 101 | try { 102 | // Get queue number from location header 103 | const parts = ctx.res.headers.location.split("/"); 104 | 105 | return next(false, parseInt(parts[parts.length - 2], 10)); 106 | } catch (err) { 107 | // ignore errors 108 | } 109 | 110 | next(); 111 | } 112 | 113 | exports.body = body; 114 | exports.bodyItem = bodyItem; 115 | exports.empty = empty; 116 | exports.exists = exists; 117 | exports.ignoreErrorForStatusCodes = ignoreErrorForStatusCodes; 118 | exports.notFound = notFound; 119 | exports.queueLocation = queueLocation; 120 | exports.require302 = require302; 121 | -------------------------------------------------------------------------------- /lib/node.js: -------------------------------------------------------------------------------- 1 | const middleware = require("./middleware"); 2 | const utils = require("./utils"); 3 | 4 | class Node { 5 | constructor(jenkins) { 6 | this.jenkins = jenkins; 7 | } 8 | 9 | /** 10 | * Get or update config 11 | */ 12 | async config(name, xml, opts) { 13 | opts = utils.parse([...arguments], "name", "xml"); 14 | 15 | this.jenkins._log(["debug", "node", "config"], opts); 16 | 17 | const req = { name: "node.config" }; 18 | 19 | utils.options(req, opts); 20 | 21 | try { 22 | if (!opts.name) throw new Error("name required"); 23 | 24 | req.path = "/computer/{name}/config.xml"; 25 | req.params = { 26 | name: opts.name === "master" ? "(master)" : opts.name, 27 | }; 28 | 29 | if (opts.xml) { 30 | if (opts.name === "master") { 31 | throw new Error("master not supported"); 32 | } 33 | 34 | req.method = "POST"; 35 | req.headers = { "content-type": "text/xml; charset=utf-8" }; 36 | req.body = Buffer.from(opts.xml); 37 | } else { 38 | req.method = "GET"; 39 | } 40 | } catch (err) { 41 | throw this.jenkins._err(err, req); 42 | } 43 | 44 | return await this.jenkins._request( 45 | req, 46 | middleware.notFound("node " + opts.name), 47 | (ctx, next) => { 48 | if (ctx.err || opts.xml) return middleware.empty(ctx, next); 49 | 50 | next(false, ctx.res.body.toString("utf8")); 51 | } 52 | ); 53 | } 54 | 55 | /** 56 | * Create node 57 | */ 58 | async create(name, opts) { 59 | opts = utils.parse([...arguments], "name"); 60 | 61 | opts.type = opts.type || "hudson.slaves.DumbSlave$DescriptorImpl"; 62 | opts.retentionStrategy = opts.retentionStrategy || { 63 | "stapler-class": "hudson.slaves.RetentionStrategy$Always", 64 | }; 65 | opts.nodeProperties = opts.nodeProperties || { 66 | "stapler-class-bag": "true", 67 | }; 68 | opts.launcher = opts.launcher || { 69 | "stapler-class": "hudson.slaves.JNLPLauncher", 70 | }; 71 | opts.numExecutors = opts.hasOwnProperty("numExecutors") 72 | ? opts.numExecutors 73 | : 2; 74 | opts.remoteFS = opts.remoteFS || "/var/lib/jenkins"; 75 | opts.mode = opts.mode || (opts.exclusive ? "EXCLUSIVE" : "NORMAL"); 76 | 77 | this.jenkins._log(["debug", "node", "create"], opts); 78 | 79 | const req = { name: "node.create" }; 80 | 81 | utils.options(req, opts); 82 | 83 | try { 84 | if (!opts.name) throw new Error("name required"); 85 | 86 | req.path = "/computer/doCreateItem"; 87 | req.query.name = opts.name; 88 | req.query.type = opts.type; 89 | req.query.json = JSON.stringify({ 90 | name: opts.name, 91 | nodeDescription: opts.nodeDescription, 92 | numExecutors: opts.numExecutors, 93 | remoteFS: opts.remoteFS, 94 | labelString: opts.labelString, 95 | mode: opts.mode, 96 | type: opts.type, 97 | retentionStrategy: opts.retentionStrategy, 98 | nodeProperties: opts.nodeProperties, 99 | launcher: opts.launcher, 100 | }); 101 | } catch (err) { 102 | throw this.jenkins._err(err, req); 103 | } 104 | 105 | return await this.jenkins._post( 106 | req, 107 | middleware.require302("failed to create: " + opts.name), 108 | middleware.empty 109 | ); 110 | } 111 | 112 | /** 113 | * Destroy node 114 | */ 115 | async destroy(name, opts) { 116 | opts = utils.parse([...arguments], "name"); 117 | 118 | this.jenkins._log(["debug", "node", "destroy"], opts); 119 | 120 | const req = { name: "node.destroy" }; 121 | 122 | utils.options(req, opts); 123 | 124 | try { 125 | if (!opts.name) throw new Error("name required"); 126 | 127 | req.path = "/computer/{name}/doDelete"; 128 | req.params = { name: opts.name }; 129 | } catch (err) { 130 | throw this.jenkins._err(err, req); 131 | } 132 | 133 | return await this.jenkins._post( 134 | req, 135 | middleware.notFound(opts.name), 136 | middleware.require302("failed to delete: " + opts.name), 137 | middleware.empty 138 | ); 139 | } 140 | 141 | async delete(...args) { 142 | return await this.destroy(...args); 143 | } 144 | 145 | /** 146 | * Disconnect node call 147 | */ 148 | async doDisconnect(name, opts) { 149 | opts = utils.parse([...arguments], "name"); 150 | 151 | this.jenkins._log(["debug", "node", "doDisconnect"], opts); 152 | 153 | const req = { name: "node.doDisconnect" }; 154 | 155 | utils.options(req, opts); 156 | 157 | try { 158 | if (!opts.name) throw new Error("name required"); 159 | 160 | req.path = "/computer/{name}/doDisconnect"; 161 | req.params = { name: opts.name }; 162 | req.query.offlineMessage = opts.message || ""; 163 | } catch (err) { 164 | throw this.jenkins._err(err, req); 165 | } 166 | 167 | return await this.jenkins._post( 168 | req, 169 | middleware.notFound(opts.name), 170 | middleware.require302("failed to disconnect: " + opts.name), 171 | middleware.empty 172 | ); 173 | } 174 | 175 | /** 176 | * Toggle offline 177 | */ 178 | async toggleOffline(name, message, opts) { 179 | opts = utils.parse([...arguments], "name", "message"); 180 | 181 | this.jenkins._log(["debug", "node", "toggleOffline"], opts); 182 | 183 | const req = { name: "node.toggleOffline" }; 184 | 185 | utils.options(req, opts); 186 | 187 | try { 188 | if (!opts.name) throw new Error("name required"); 189 | 190 | req.path = "/computer/{name}/toggleOffline"; 191 | req.params = { name: opts.name }; 192 | req.query.offlineMessage = opts.message || ""; 193 | } catch (err) { 194 | throw this.jenkins._err(err, req); 195 | } 196 | 197 | return await this.jenkins._post( 198 | req, 199 | middleware.notFound(opts.name), 200 | middleware.require302("failed to toggle offline: " + opts.name), 201 | middleware.empty 202 | ); 203 | } 204 | 205 | /** 206 | * Change offline message 207 | */ 208 | async changeOfflineCause(name, message, opts) { 209 | opts = utils.parse([...arguments], "name", "message"); 210 | 211 | opts.message = opts.message || ""; 212 | 213 | this.jenkins._log(["debug", "node", "changeOfflineCause"], opts); 214 | 215 | const req = { name: "node.changeOfflineCause" }; 216 | 217 | utils.options(req, opts); 218 | 219 | try { 220 | if (!opts.name) throw new Error("name required"); 221 | 222 | req.path = "/computer/{name}/changeOfflineCause"; 223 | req.params = { name: opts.name }; 224 | req.type = "form"; 225 | req.body = { 226 | offlineMessage: opts.message, 227 | json: JSON.stringify({ 228 | offlineMessage: opts.message, 229 | }), 230 | Submit: "Update reason", 231 | }; 232 | } catch (err) { 233 | throw this.jenkins._err(err, req); 234 | } 235 | 236 | return await this.jenkins._post( 237 | req, 238 | middleware.notFound(opts.name), 239 | middleware.require302("failed to update offline message: " + opts.name), 240 | middleware.empty 241 | ); 242 | } 243 | 244 | /** 245 | * Disconnect node 246 | */ 247 | async disconnect(name, message, opts) { 248 | opts = utils.parse([...arguments], "name", "message"); 249 | 250 | this.jenkins._log(["debug", "node", "disconnect"], opts); 251 | 252 | if (!opts.name) { 253 | throw this.jenkins._err("name required", { name: "node.disconnect" }); 254 | } 255 | 256 | const node = await this.get(opts.name); 257 | 258 | if (node && node.offline) { 259 | await this.toggleOffline({ name: opts.name, message: opts.message }); 260 | return; 261 | } 262 | 263 | await this.doDisconnect({ name: opts.name, message: opts.message }); 264 | } 265 | 266 | /** 267 | * Disable node 268 | */ 269 | async disable(name, message, opts) { 270 | opts = utils.parse([...arguments], "name", "message"); 271 | 272 | this.jenkins._log(["debug", "node", "disable"], opts); 273 | 274 | if (!opts.name) { 275 | throw this.jenkins._err("name required", { name: "node.disable" }); 276 | } 277 | 278 | const node = await this.get(opts.name); 279 | 280 | if (node && node.temporarilyOffline) { 281 | if (node.offlineCauseReason !== opts.message) { 282 | await this.changeOfflineCause({ 283 | name: opts.name, 284 | message: opts.message, 285 | }); 286 | } 287 | 288 | return; 289 | } 290 | 291 | await this.toggleOffline({ name: opts.name, message: opts.message }); 292 | } 293 | 294 | /** 295 | * Enable node 296 | */ 297 | async enable(name, opts) { 298 | opts = utils.parse([...arguments], "name"); 299 | 300 | this.jenkins._log(["debug", "node", "enable"], opts); 301 | 302 | if (!opts.name) { 303 | throw this.jenkins._err("name required", { name: "node.enable" }); 304 | } 305 | 306 | const node = await this.get(opts.name); 307 | if (node.temporarilyOffline) { 308 | await this.toggleOffline({ name: opts.name, message: "" }); 309 | } 310 | } 311 | 312 | /** 313 | * Node exists 314 | */ 315 | async exists(name, opts) { 316 | opts = utils.parse([...arguments], "name"); 317 | 318 | this.jenkins._log(["debug", "build", "exists"], opts); 319 | 320 | const req = { name: "node.exists" }; 321 | 322 | utils.options(req, opts); 323 | 324 | try { 325 | if (!opts.name) throw new Error("name required"); 326 | 327 | req.path = "/computer/{name}/api/json"; 328 | req.params = { 329 | name: opts.name === "master" ? "(master)" : opts.name, 330 | }; 331 | } catch (err) { 332 | throw this.jenkins._err(err, req); 333 | } 334 | 335 | return await this.jenkins._head(req, middleware.exists); 336 | } 337 | 338 | /** 339 | * Node details 340 | */ 341 | async get(name, opts) { 342 | opts = utils.parse([...arguments], "name"); 343 | 344 | this.jenkins._log(["debug", "node", "get"], opts); 345 | 346 | const req = { name: "node.get" }; 347 | 348 | utils.options(req, opts); 349 | 350 | try { 351 | if (!opts.name) throw new Error("name required"); 352 | 353 | req.path = "/computer/{name}/api/json"; 354 | req.params = { 355 | name: opts.name === "master" ? "(master)" : opts.name, 356 | }; 357 | } catch (err) { 358 | throw this.jenkins._err(err, req); 359 | } 360 | 361 | return await this.jenkins._get( 362 | req, 363 | middleware.notFound(opts.name), 364 | middleware.body 365 | ); 366 | } 367 | 368 | /** 369 | * List nodes 370 | */ 371 | async list(opts) { 372 | opts = utils.parse([...arguments]); 373 | 374 | this.jenkins._log(["debug", "node", "list"], opts); 375 | 376 | const req = { 377 | name: "node.list", 378 | path: "/computer/api/json", 379 | }; 380 | 381 | utils.options(req, opts); 382 | 383 | if (opts.full === true) { 384 | return await this.jenkins._get(req, middleware.body); 385 | } else { 386 | return await this.jenkins._get(req, middleware.bodyItem("computer")); 387 | } 388 | } 389 | } 390 | 391 | exports.Node = Node; 392 | -------------------------------------------------------------------------------- /lib/plugin.js: -------------------------------------------------------------------------------- 1 | const middleware = require("./middleware"); 2 | const utils = require("./utils"); 3 | 4 | class Plugin { 5 | constructor(jenkins) { 6 | this.jenkins = jenkins; 7 | } 8 | 9 | /** 10 | * List plugins 11 | */ 12 | async list(opts) { 13 | opts = utils.parse([...arguments]); 14 | 15 | // depth of 0 is useless for plugins 16 | if (typeof opts.depth === "undefined") opts.depth = 1; 17 | 18 | this.jenkins._log(["debug", "plugin", "list"], opts); 19 | 20 | const req = { 21 | name: "plugin.list", 22 | path: "/pluginManager/api/json", 23 | }; 24 | 25 | utils.options(req, opts); 26 | 27 | return await this.jenkins._get(req, middleware.bodyItem("plugins")); 28 | } 29 | } 30 | 31 | exports.Plugin = Plugin; 32 | -------------------------------------------------------------------------------- /lib/queue.js: -------------------------------------------------------------------------------- 1 | const middleware = require("./middleware"); 2 | const utils = require("./utils"); 3 | 4 | class Queue { 5 | constructor(jenkins) { 6 | this.jenkins = jenkins; 7 | } 8 | 9 | /** 10 | * List queues 11 | */ 12 | async list(opts) { 13 | opts = utils.parse([...arguments]); 14 | 15 | this.jenkins._log(["debug", "queue", "list"], opts); 16 | 17 | const req = { 18 | name: "queue.list", 19 | path: "/queue/api/json", 20 | }; 21 | 22 | utils.options(req, opts); 23 | 24 | return this.jenkins._get(req, middleware.bodyItem("items")); 25 | } 26 | 27 | /** 28 | * Get an individual queue item 29 | */ 30 | async item(number, opts) { 31 | opts = utils.parse([...arguments], "number"); 32 | 33 | this.jenkins._log(["debug", "queue", "item"], opts); 34 | 35 | const req = { 36 | name: "queue.item", 37 | path: "/queue/item/{number}/api/json", 38 | params: { 39 | number: opts.number, 40 | }, 41 | }; 42 | 43 | utils.options(req, opts); 44 | 45 | if (!opts.number) { 46 | throw this.jenkins._err(new Error("number required"), req); 47 | } 48 | 49 | return await this.jenkins._get(req, middleware.body); 50 | } 51 | 52 | /** 53 | * Cancel queue item 54 | */ 55 | async cancel(number, opts) { 56 | opts = utils.parse([...arguments], "number"); 57 | 58 | this.jenkins._log(["debug", "queue", "cancel"], opts); 59 | 60 | const req = { name: "queue.cancel" }; 61 | 62 | utils.options(req, opts); 63 | 64 | try { 65 | if (!opts.number) throw new Error("number required"); 66 | 67 | req.path = "/queue/item/{number}/cancelQueue"; 68 | req.params = { number: opts.number }; 69 | } catch (err) { 70 | throw this.jenkins._err(err, req); 71 | } 72 | 73 | return await this.jenkins._post( 74 | req, 75 | middleware.require302("failed to cancel: " + opts.number), 76 | middleware.empty 77 | ); 78 | } 79 | } 80 | 81 | exports.Queue = Queue; 82 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const urlParse = require("url").parse; 2 | 3 | /** 4 | * Common options 5 | */ 6 | function options(req, opts) { 7 | if (!req.query) req.query = {}; 8 | 9 | if (typeof opts.depth === "number") { 10 | req.query.depth = opts.depth; 11 | } 12 | 13 | if (typeof opts.tree === "string") { 14 | req.query.tree = opts.tree; 15 | } 16 | 17 | return opts; 18 | } 19 | 20 | /** 21 | * Raw path param 22 | */ 23 | class RawParam { 24 | constructor(value) { 25 | this.encode = false; 26 | this.value = value || ""; 27 | } 28 | 29 | toString() { 30 | return this.value; 31 | } 32 | } 33 | 34 | /** 35 | * Parse job name from URL 36 | */ 37 | function parseName(value) { 38 | const jobParts = []; 39 | 40 | const pathParts = (urlParse(value).pathname || "").split("/").filter(Boolean); 41 | let state = 0; 42 | let part; 43 | 44 | // iterate until we find our first job, then collect the continuous job parts 45 | // ['foo', 'job', 'a', 'job', 'b', 'bar', 'job', 'c'] => ['a', 'b'] 46 | loop: for (let i = 0; i < pathParts.length; i++) { 47 | part = pathParts[i]; 48 | 49 | switch (state) { 50 | case 0: 51 | if (part === "job") state = 2; 52 | break; 53 | case 1: 54 | if (part !== "job") break loop; 55 | state = 2; 56 | break; 57 | case 2: 58 | jobParts.push(part); 59 | state = 1; 60 | break; 61 | } 62 | } 63 | 64 | return jobParts.map(decodeURIComponent); 65 | } 66 | 67 | /** 68 | * Path for folder plugin 69 | */ 70 | class FolderPath { 71 | SEP = "/job/"; 72 | 73 | constructor(value) { 74 | if (Array.isArray(value)) { 75 | this.value = value; 76 | } else if (typeof value === "string") { 77 | if (value.match("^https?://")) { 78 | this.value = parseName(value); 79 | } else { 80 | this.value = value.split("/").filter(Boolean); 81 | } 82 | } else { 83 | this.value = []; 84 | } 85 | } 86 | 87 | isEmpty() { 88 | return !this.value.length; 89 | } 90 | 91 | name() { 92 | return this.value[this.value.length - 1] || ""; 93 | } 94 | 95 | path(sep) { 96 | if (this.isEmpty()) return new RawParam(); 97 | if (!sep) sep = this.SEP; 98 | 99 | return new RawParam(sep + this.value.map(encodeURIComponent).join(sep)); 100 | } 101 | 102 | parent() { 103 | return new FolderPath( 104 | this.value.slice(0, Math.max(0, this.value.length - 1)) 105 | ); 106 | } 107 | 108 | dir() { 109 | return this.parent().path(); 110 | } 111 | } 112 | 113 | /** 114 | * Default crumb issuser 115 | */ 116 | async function crumbIssuer(jenkins) { 117 | const data = await jenkins.crumbIssuer.get(); 118 | 119 | if (!data || !data.crumbRequestField || !data.crumb) { 120 | throw new Error("Failed to get crumb"); 121 | } 122 | 123 | return { 124 | headerName: data.crumbRequestField, 125 | headerValue: data.crumb, 126 | cookies: data.cookies, 127 | }; 128 | } 129 | 130 | /** 131 | * Check if object is file like 132 | */ 133 | function isFileLike(v) { 134 | return ( 135 | Buffer.isBuffer(v) || 136 | (v !== null && 137 | typeof v === "object" && 138 | typeof v.pipe === "function" && 139 | v.readable !== false) 140 | ); 141 | } 142 | 143 | /** 144 | * Parse arguments 145 | */ 146 | function parse(args, ...names) { 147 | let last = args.length - 1; 148 | 149 | let opts; 150 | if (typeof args[last] === "object") { 151 | if (args[last] === null) { 152 | opts = {}; 153 | } else { 154 | opts = clone(args[last]); 155 | } 156 | last--; 157 | } else { 158 | opts = {}; 159 | } 160 | 161 | for (let i = 0; i <= last; i++) { 162 | const name = names[i]; 163 | const arg = args[i]; 164 | 165 | if (name && arg !== null && arg !== undefined) { 166 | opts[name] = arg; 167 | } 168 | } 169 | 170 | return opts; 171 | } 172 | 173 | /** 174 | * Shallow clone 175 | */ 176 | function clone(src) { 177 | return Object.assign({}, src); 178 | } 179 | 180 | exports.options = options; 181 | exports.folderPath = (value) => new FolderPath(value); 182 | exports.crumbIssuer = crumbIssuer; 183 | exports.isFileLike = isFileLike; 184 | exports.clone = clone; 185 | exports.parse = parse; 186 | -------------------------------------------------------------------------------- /lib/view.js: -------------------------------------------------------------------------------- 1 | const middleware = require("./middleware"); 2 | const utils = require("./utils"); 3 | 4 | class View { 5 | constructor(jenkins) { 6 | this.jenkins = jenkins; 7 | } 8 | 9 | /** 10 | * Create new view 11 | */ 12 | async create(name, type, opts) { 13 | opts = utils.parse([...arguments], "name", "type"); 14 | if (opts.name && !opts.type) opts.type = "list"; 15 | 16 | this.jenkins._log(["debug", "view", "create"], opts); 17 | 18 | const req = { name: "view.create" }; 19 | 20 | utils.options(req, opts); 21 | 22 | const shortcuts = { 23 | list: "hudson.model.ListView", 24 | my: "hudson.model.MyView", 25 | }; 26 | 27 | try { 28 | const folder = utils.folderPath(opts.name); 29 | const mode = shortcuts[opts.type] || opts.type; 30 | 31 | if (folder.isEmpty()) throw new Error("name required"); 32 | if (!opts.type) throw new Error("type required"); 33 | 34 | req.path = "{dir}/createView"; 35 | req.type = "form"; 36 | req.params = { dir: folder.dir() }; 37 | req.body = { 38 | name: folder.name(), 39 | mode: mode, 40 | json: JSON.stringify({ 41 | name: folder.name(), 42 | mode: mode, 43 | }), 44 | }; 45 | } catch (err) { 46 | throw this.jenkins._err(err, req); 47 | } 48 | 49 | return await this.jenkins._post( 50 | req, 51 | middleware.require302("failed to create: " + opts.name), 52 | middleware.empty 53 | ); 54 | } 55 | 56 | /** 57 | * Config list view 58 | */ 59 | async config(name, xml, opts) { 60 | opts = utils.parse([...arguments], "name", "xml"); 61 | 62 | this.jenkins._log(["debug", "view", "config"], opts); 63 | 64 | const req = { 65 | path: "{dir}/view/{name}/config.xml", 66 | name: "view.config", 67 | }; 68 | 69 | utils.options(req, opts); 70 | 71 | try { 72 | const folder = utils.folderPath(opts.name); 73 | 74 | if (folder.isEmpty()) throw new Error("name required"); 75 | 76 | req.params = { dir: folder.dir(), name: folder.name() }; 77 | 78 | if (opts.xml) { 79 | req.method = "POST"; 80 | req.headers = { "content-type": "text/xml; charset=utf-8" }; 81 | req.body = Buffer.from(opts.xml); 82 | } else { 83 | req.method = "GET"; 84 | } 85 | } catch (err) { 86 | throw this.jenkins._err(err, req); 87 | } 88 | 89 | return await this.jenkins._request( 90 | req, 91 | middleware.notFound("view " + opts.name), 92 | (ctx, next) => { 93 | if (ctx.err || opts.xml) return middleware.empty(ctx, next); 94 | 95 | next(false, ctx.res.body.toString("utf8")); 96 | } 97 | ); 98 | } 99 | 100 | /** 101 | * Destroy view 102 | */ 103 | async destroy(name, opts) { 104 | opts = utils.parse([...arguments], "name"); 105 | 106 | this.jenkins._log(["debug", "view", "destroy"], opts); 107 | 108 | const req = { name: "view.destroy" }; 109 | 110 | utils.options(req, opts); 111 | 112 | try { 113 | const folder = utils.folderPath(opts.name); 114 | 115 | if (folder.isEmpty()) throw new Error("name required"); 116 | 117 | req.path = "{dir}/view/{name}/doDelete"; 118 | req.params = { dir: folder.dir(), name: folder.name() }; 119 | } catch (err) { 120 | throw this.jenkins._err(err, req); 121 | } 122 | 123 | return await this.jenkins._post( 124 | req, 125 | middleware.notFound(opts.name), 126 | middleware.require302("failed to delete: " + opts.name), 127 | middleware.empty 128 | ); 129 | } 130 | 131 | async delete(...args) { 132 | return await this.destroy(...args); 133 | } 134 | 135 | /** 136 | * View exists 137 | */ 138 | async exists(name, opts) { 139 | opts = utils.parse([...arguments], "name"); 140 | 141 | this.jenkins._log(["debug", "view", "exists"], opts); 142 | 143 | const req = { name: "view.exists" }; 144 | 145 | utils.options(req, opts); 146 | 147 | try { 148 | const folder = utils.folderPath(opts.name); 149 | 150 | if (folder.isEmpty()) throw new Error("name required"); 151 | 152 | req.path = "{dir}/view/{name}/api/json"; 153 | req.params = { dir: folder.dir(), name: folder.name() }; 154 | } catch (err) { 155 | throw this.jenkins._err(err, req); 156 | } 157 | 158 | return await this.jenkins._head(req, middleware.exists); 159 | } 160 | 161 | /** 162 | * View details 163 | */ 164 | async get(name, opts) { 165 | opts = utils.parse([...arguments], "name"); 166 | 167 | this.jenkins._log(["debug", "view", "get"], opts); 168 | 169 | const req = { name: "view.get" }; 170 | 171 | utils.options(req, opts); 172 | 173 | try { 174 | const folder = utils.folderPath(opts.name); 175 | 176 | if (folder.isEmpty()) throw new Error("name required"); 177 | 178 | req.path = "{dir}/view/{name}/api/json"; 179 | req.params = { dir: folder.dir(), name: folder.name() }; 180 | } catch (err) { 181 | throw this.jenkins._err(err, req); 182 | } 183 | 184 | return await this.jenkins._get( 185 | req, 186 | middleware.notFound(opts.name), 187 | middleware.body 188 | ); 189 | } 190 | 191 | /** 192 | * List views 193 | */ 194 | async list(name, opts) { 195 | opts = utils.parse([...arguments], "name"); 196 | 197 | this.jenkins._log(["debug", "view", "list"], opts); 198 | 199 | const req = { 200 | name: "view.list", 201 | path: "{folder}/api/json", 202 | }; 203 | 204 | try { 205 | const folder = utils.folderPath(opts.name); 206 | 207 | req.params = { folder: folder.path() }; 208 | } catch (err) { 209 | throw this.jenkins._err(err, req); 210 | } 211 | 212 | utils.options(req, opts); 213 | 214 | return await this.jenkins._get( 215 | req, 216 | (ctx, next) => { 217 | if (ctx.err) return next(); 218 | 219 | if (!ctx.res.body || !Array.isArray(ctx.res.body.views)) { 220 | ctx.err = new Error("returned bad data"); 221 | } 222 | 223 | next(); 224 | }, 225 | middleware.bodyItem("views") 226 | ); 227 | } 228 | 229 | /** 230 | * Add job 231 | */ 232 | async add(name, job, opts) { 233 | opts = utils.parse([...arguments], "name", "job"); 234 | 235 | this.jenkins._log(["debug", "view", "add"], opts); 236 | 237 | const req = { 238 | path: "{dir}/view/{name}/addJobToView", 239 | query: { name: opts.job }, 240 | type: "form", 241 | name: "view.add", 242 | body: {}, 243 | }; 244 | 245 | utils.options(req, opts); 246 | 247 | try { 248 | const folder = utils.folderPath(opts.name); 249 | 250 | if (folder.isEmpty()) throw new Error("name required"); 251 | if (!opts.job) throw new Error("job required"); 252 | 253 | req.params = { dir: folder.dir(), name: folder.name() }; 254 | } catch (err) { 255 | throw this.jenkins._err(err, req); 256 | } 257 | 258 | return await this.jenkins._post(req, middleware.empty); 259 | } 260 | 261 | /** 262 | * Remove job 263 | */ 264 | async remove(name, job, opts) { 265 | opts = utils.parse([...arguments], "name", "job"); 266 | 267 | this.jenkins._log(["debug", "view", "remove"], opts); 268 | 269 | const req = { 270 | path: "{dir}/view/{name}/removeJobFromView", 271 | query: { name: opts.job }, 272 | type: "form", 273 | name: "view.remove", 274 | body: {}, 275 | }; 276 | 277 | utils.options(req, opts); 278 | 279 | try { 280 | const folder = utils.folderPath(opts.name); 281 | 282 | if (folder.isEmpty()) throw new Error("name required"); 283 | if (!opts.job) throw new Error("job required"); 284 | 285 | req.params = { dir: folder.dir(), name: folder.name() }; 286 | } catch (err) { 287 | throw this.jenkins._err(err, req); 288 | } 289 | 290 | return await this.jenkins._post(req, middleware.empty); 291 | } 292 | } 293 | 294 | exports.View = View; 295 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jenkins", 3 | "description": "Jenkins client", 4 | "version": "1.1.0", 5 | "main": "./lib", 6 | "files": [ 7 | "./lib" 8 | ], 9 | "dependencies": { 10 | "papi": "^1.1.0" 11 | }, 12 | "devDependencies": { 13 | "async": "^3.2.4", 14 | "debug": "^4.3.1", 15 | "fixturefiles": "^0.3.0", 16 | "form-data": "^4.0.0", 17 | "mocha": "^10.0.0", 18 | "nock": "^13.2.9", 19 | "node-uuid": "^1.4.8", 20 | "nyc": "^15.1.0", 21 | "prettier": "^2.7.1", 22 | "should": "^13.2.1", 23 | "sinon": "^14.0.0" 24 | }, 25 | "scripts": { 26 | "format": "prettier -w .", 27 | "test": "prettier -c . && nyc mocha -- --recursive --check-leaks --timeout 15000", 28 | "acceptance": "NOCK_OFF=true NOCK_REC=false JENKINS_TEST_URL=http://admin:admin@localhost:8080 mocha --recursive --check-leaks --timeout 15000" 29 | }, 30 | "keywords": [ 31 | "jenkins" 32 | ], 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/silas/node-jenkins.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/silas/node-jenkins/issues" 39 | }, 40 | "author": "Silas Sewell ", 41 | "license": "MIT" 42 | } 43 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jenkins/jenkins:lts-alpine 2 | 3 | ARG REMOTING_VERSION=4.14 4 | 5 | RUN curl -sSLo /usr/share/jenkins/ref/slave.jar https://repo.jenkins-ci.org/public/org/jenkins-ci/main/remoting/${REMOTING_VERSION}/remoting-${REMOTING_VERSION}.jar 6 | 7 | ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false" 8 | 9 | COPY setup.groovy /usr/share/jenkins/ref/init.groovy.d/setup.groovy 10 | COPY scriptApproval.xml /var/jenkins_home/scriptApproval.xml 11 | 12 | RUN jenkins-plugin-cli --plugins "jdk-tool script-security command-launcher cloudbees-folder credentials" 13 | -------------------------------------------------------------------------------- /test/compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | jenkins: 5 | build: 6 | context: ./ 7 | ports: 8 | - "8080:8080" 9 | -------------------------------------------------------------------------------- /test/fixtures/buildGet.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [ 3 | { 4 | "causes": [ 5 | { 6 | "shortDescription": "Started by user anonymous", 7 | "userId": null, 8 | "userName": "anonymous" 9 | } 10 | ] 11 | } 12 | ], 13 | "artifacts": [], 14 | "building": false, 15 | "description": null, 16 | "duration": 11, 17 | "estimatedDuration": 11, 18 | "executor": null, 19 | "fullDisplayName": "test-7ea39228-5841-4a0f-b83c-1d971713f9c6 #1", 20 | "id": "2014-07-25_05-15-17", 21 | "keepLog": false, 22 | "number": 1, 23 | "result": "SUCCESS", 24 | "timestamp": 1406265317652, 25 | "url": "http://localhost:8080/job/test-7ea39228-5841-4a0f-b83c-1d971713f9c6/1/", 26 | "builtOn": "", 27 | "changeSet": { 28 | "items": [], 29 | "kind": null 30 | }, 31 | "culprits": [] 32 | } 33 | -------------------------------------------------------------------------------- /test/fixtures/consoleText.txt: -------------------------------------------------------------------------------- 1 | Started by user anonymous 2 | Building in workspace /var/jenkins_home/workspace/node-jenkins 3 | Cloning the remote Git repository 4 | Cloning repository https://github.com/silas/node-jenkins 5 | > git init /var/jenkins_home/workspace/node-jenkins # timeout=10 6 | Fetching upstream changes from https://github.com/silas/node-jenkins 7 | > git --version # timeout=10 8 | > git fetch --tags --progress https://github.com/silas/node-jenkins +refs/heads/*:refs/remotes/origin/* 9 | > git config remote.origin.url https://github.com/silas/node-jenkins # timeout=10 10 | > git config remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10 11 | > git config remote.origin.url https://github.com/silas/node-jenkins # timeout=10 12 | Fetching upstream changes from https://github.com/silas/node-jenkins 13 | > git fetch --tags --progress https://github.com/silas/node-jenkins +refs/heads/*:refs/remotes/origin/* 14 | > git rev-parse refs/remotes/origin/master^{commit} # timeout=10 15 | > git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10 16 | Checking out Revision 5b8cbc40205c81914d40a0b71ee2944374eec2fe (refs/remotes/origin/master) 17 | > git config core.sparsecheckout # timeout=10 18 | > git checkout -f 5b8cbc40205c81914d40a0b71ee2944374eec2fe 19 | First time build. Skipping changelog. 20 | Finished: SUCCESS 21 | -------------------------------------------------------------------------------- /test/fixtures/credentialCreate.xml: -------------------------------------------------------------------------------- 1 | 2 | GLOBAL 3 | user-new-cred 4 | This is an example from REST API 5 | admin 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/fixtures/credentialList.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "com.cloudbees.plugins.credentials.CredentialsStoreAction$DomainWrapper", 3 | "credentials": [ 4 | { 5 | "id": "user-new-cred" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/credentialUpdate.xml: -------------------------------------------------------------------------------- 1 | 2 | GLOBAL 3 | user-new-cred 4 | This is an example from REST API 5 | updated 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/fixtures/folderCreate.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | All 9 | false 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/fixtures/jobCreate.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | before 5 | false 6 | 7 | 8 | true 9 | false 10 | false 11 | false 12 | 13 | false 14 | 15 | 16 | sleep 1 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/fixtures/jobGet.json: -------------------------------------------------------------------------------- 1 | { 2 | "upstreamProjects": [], 3 | "scm": {}, 4 | "downstreamProjects": [], 5 | "concurrentBuild": false, 6 | "queueItem": null, 7 | "property": [], 8 | "nextBuildNumber": 1, 9 | "lastUnsuccessfulBuild": null, 10 | "lastUnstableBuild": null, 11 | "lastSuccessfulBuild": null, 12 | "lastStableBuild": null, 13 | "builds": [], 14 | "buildable": true, 15 | "url": "http://localhost:8080/job/test-ff52154b-ceb3-4b21-af1b-a4d48159a48f/", 16 | "name": "test-ff52154b-ceb3-4b21-af1b-a4d48159a48f", 17 | "displayNameOrNull": null, 18 | "displayName": "test-ff52154b-ceb3-4b21-af1b-a4d48159a48f", 19 | "description": "before", 20 | "actions": [], 21 | "color": "notbuilt", 22 | "firstBuild": null, 23 | "healthReport": [], 24 | "inQueue": false, 25 | "keepDependencies": false, 26 | "lastBuild": null, 27 | "lastCompletedBuild": null, 28 | "lastFailedBuild": null 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/jobGetDisabled.json: -------------------------------------------------------------------------------- 1 | { 2 | "upstreamProjects": [], 3 | "scm": {}, 4 | "downstreamProjects": [], 5 | "concurrentBuild": false, 6 | "queueItem": null, 7 | "property": [], 8 | "nextBuildNumber": 1, 9 | "lastUnsuccessfulBuild": null, 10 | "lastUnstableBuild": null, 11 | "lastSuccessfulBuild": null, 12 | "lastStableBuild": null, 13 | "builds": [], 14 | "buildable": false, 15 | "url": "http://localhost:8080/job/test-ff52154b-ceb3-4b21-af1b-a4d48159a48f/", 16 | "name": "test-ff52154b-ceb3-4b21-af1b-a4d48159a48f", 17 | "displayNameOrNull": null, 18 | "displayName": "test-ff52154b-ceb3-4b21-af1b-a4d48159a48f", 19 | "description": "before", 20 | "actions": [], 21 | "color": "disabled", 22 | "firstBuild": null, 23 | "healthReport": [], 24 | "inQueue": false, 25 | "keepDependencies": false, 26 | "lastBuild": null, 27 | "lastCompletedBuild": null, 28 | "lastFailedBuild": null 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/jobList.json: -------------------------------------------------------------------------------- 1 | { 2 | "views": [ 3 | { 4 | "url": "http://localhost:8080/", 5 | "name": "All" 6 | } 7 | ], 8 | "useSecurity": false, 9 | "useCrumbs": false, 10 | "unlabeledLoad": {}, 11 | "slaveAgentPort": 0, 12 | "quietingDown": false, 13 | "primaryView": { 14 | "url": "http://localhost:8080/", 15 | "name": "All" 16 | }, 17 | "assignedLabels": [{}], 18 | "mode": "NORMAL", 19 | "nodeDescription": "the master Jenkins node", 20 | "nodeName": "", 21 | "numExecutors": 2, 22 | "description": null, 23 | "jobs": [ 24 | { 25 | "color": "blue", 26 | "url": "http://localhost:8080/job/asdf/", 27 | "name": "asdf" 28 | }, 29 | { 30 | "color": "notbuilt", 31 | "url": "http://localhost:8080/job/test-e474800e-9d04-437a-8dd3-c29c7156185e/", 32 | "name": "test-e474800e-9d04-437a-8dd3-c29c7156185e" 33 | } 34 | ], 35 | "overallLoad": {} 36 | } 37 | -------------------------------------------------------------------------------- /test/fixtures/jobUpdate.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | after 5 | false 6 | 7 | 8 | true 9 | false 10 | false 11 | false 12 | 13 | false 14 | 15 | 16 | export FOO=bar 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/fixtures/labelGet.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.model.labels.LabelAtom", 3 | "actions": [], 4 | "busyExecutors": 0, 5 | "clouds": [], 6 | "description": null, 7 | "idleExecutors": 2, 8 | "loadStatistics": { 9 | "_class": "hudson.model.Label$1" 10 | }, 11 | "name": "master", 12 | "nodes": [ 13 | { 14 | "_class": "hudson.model.Hudson", 15 | "nodeName": "" 16 | } 17 | ], 18 | "offline": false, 19 | "tiedJobs": [], 20 | "totalExecutors": 2, 21 | "propertiesList": [] 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/nodeConfigMaster.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1.571 5 | 3 6 | NORMAL 7 | true 8 | 9 | 10 | false 11 | 12 | ${JENKINS_HOME}/workspace/${ITEM_FULLNAME} 13 | ${ITEM_ROOTDIR}/builds 14 | 15 | 16 | 17 | 18 | 19 | 0 20 | 0 21 | 22 | 23 | 24 | All 25 | false 26 | false 27 | 28 | 29 | 30 | All 31 | 0 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/fixtures/nodeCreateQuery.txt: -------------------------------------------------------------------------------- 1 | name={name}&type=hudson.slaves.DumbSlave%24DescriptorImpl&json=%7B%22name%22%3A%22{name}%22%2C%22numExecutors%22%3A2%2C%22remoteFS%22%3A%22%2Fvar%2Flib%2Fjenkins%22%2C%22mode%22%3A%22NORMAL%22%2C%22type%22%3A%22hudson.slaves.DumbSlave%24DescriptorImpl%22%2C%22retentionStrategy%22%3A%7B%22stapler-class%22%3A%22hudson.slaves.RetentionStrategy%24Always%22%7D%2C%22nodeProperties%22%3A%7B%22stapler-class-bag%22%3A%22true%22%7D%2C%22launcher%22%3A%7B%22stapler-class%22%3A%22hudson.slaves.JNLPLauncher%22%7D%7D 2 | -------------------------------------------------------------------------------- /test/fixtures/nodeGet.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "displayName": "test-node-a532b0f8-6079-4efa-80ea-ad61c05bef5b", 4 | "executors": [{}, {}], 5 | "icon": "computer-x.png", 6 | "idle": true, 7 | "jnlpAgent": true, 8 | "launchSupported": false, 9 | "loadStatistics": {}, 10 | "manualLaunchAllowed": true, 11 | "monitorData": { 12 | "hudson.node_monitors.SwapSpaceMonitor": null, 13 | "hudson.node_monitors.ArchitectureMonitor": null, 14 | "hudson.node_monitors.ResponseTimeMonitor": { 15 | "average": 5000 16 | }, 17 | "hudson.node_monitors.TemporarySpaceMonitor": null, 18 | "hudson.node_monitors.DiskSpaceMonitor": null, 19 | "hudson.node_monitors.ClockMonitor": null 20 | }, 21 | "numExecutors": 2, 22 | "offline": true, 23 | "offlineCause": null, 24 | "offlineCauseReason": "", 25 | "oneOffExecutors": [], 26 | "temporarilyOffline": false 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/nodeGetOffline.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "displayName": "test-node-a532b0f8-6079-4efa-80ea-ad61c05bef5b", 4 | "executors": [{}, {}], 5 | "icon": "computer-x.png", 6 | "idle": true, 7 | "jnlpAgent": true, 8 | "launchSupported": false, 9 | "loadStatistics": {}, 10 | "manualLaunchAllowed": true, 11 | "monitorData": { 12 | "hudson.node_monitors.SwapSpaceMonitor": null, 13 | "hudson.node_monitors.ArchitectureMonitor": null, 14 | "hudson.node_monitors.ResponseTimeMonitor": { 15 | "average": 5000 16 | }, 17 | "hudson.node_monitors.TemporarySpaceMonitor": null, 18 | "hudson.node_monitors.DiskSpaceMonitor": null, 19 | "hudson.node_monitors.ClockMonitor": null 20 | }, 21 | "numExecutors": 2, 22 | "offline": true, 23 | "offlineCause": {}, 24 | "offlineCauseReason": "away", 25 | "oneOffExecutors": [], 26 | "temporarilyOffline": false 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/nodeGetOfflineUpdate.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "displayName": "test-node-a532b0f8-6079-4efa-80ea-ad61c05bef5b", 4 | "executors": [{}, {}], 5 | "icon": "computer-x.png", 6 | "idle": true, 7 | "jnlpAgent": true, 8 | "launchSupported": false, 9 | "loadStatistics": {}, 10 | "manualLaunchAllowed": true, 11 | "monitorData": { 12 | "hudson.node_monitors.SwapSpaceMonitor": null, 13 | "hudson.node_monitors.ArchitectureMonitor": null, 14 | "hudson.node_monitors.ResponseTimeMonitor": { 15 | "average": 5000 16 | }, 17 | "hudson.node_monitors.TemporarySpaceMonitor": null, 18 | "hudson.node_monitors.DiskSpaceMonitor": null, 19 | "hudson.node_monitors.ClockMonitor": null 20 | }, 21 | "numExecutors": 2, 22 | "offline": true, 23 | "offlineCause": {}, 24 | "offlineCauseReason": "update", 25 | "oneOffExecutors": [], 26 | "temporarilyOffline": false 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/nodeGetOnline.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "displayName": "test-node-a532b0f8-6079-4efa-80ea-ad61c05bef5b", 4 | "executors": [{}, {}], 5 | "icon": "computer-x.png", 6 | "idle": true, 7 | "jnlpAgent": true, 8 | "launchSupported": false, 9 | "loadStatistics": {}, 10 | "manualLaunchAllowed": true, 11 | "monitorData": { 12 | "hudson.node_monitors.SwapSpaceMonitor": null, 13 | "hudson.node_monitors.ArchitectureMonitor": null, 14 | "hudson.node_monitors.ResponseTimeMonitor": { 15 | "average": 5000 16 | }, 17 | "hudson.node_monitors.TemporarySpaceMonitor": null, 18 | "hudson.node_monitors.DiskSpaceMonitor": null, 19 | "hudson.node_monitors.ClockMonitor": null 20 | }, 21 | "numExecutors": 2, 22 | "offline": false, 23 | "offlineCause": null, 24 | "offlineCauseReason": "", 25 | "oneOffExecutors": [], 26 | "temporarilyOffline": false 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/nodeGetTempOffline.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "displayName": "test-node-a532b0f8-6079-4efa-80ea-ad61c05bef5b", 4 | "executors": [{}, {}], 5 | "icon": "computer-x.png", 6 | "idle": true, 7 | "jnlpAgent": true, 8 | "launchSupported": false, 9 | "loadStatistics": {}, 10 | "manualLaunchAllowed": true, 11 | "monitorData": { 12 | "hudson.node_monitors.SwapSpaceMonitor": null, 13 | "hudson.node_monitors.ArchitectureMonitor": null, 14 | "hudson.node_monitors.ResponseTimeMonitor": { 15 | "average": 5000 16 | }, 17 | "hudson.node_monitors.TemporarySpaceMonitor": null, 18 | "hudson.node_monitors.DiskSpaceMonitor": null, 19 | "hudson.node_monitors.ClockMonitor": null 20 | }, 21 | "numExecutors": 2, 22 | "offline": true, 23 | "offlineCause": {}, 24 | "offlineCauseReason": "away", 25 | "oneOffExecutors": [], 26 | "temporarilyOffline": true 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/nodeGetTempOfflineUpdate.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "displayName": "test-node-a532b0f8-6079-4efa-80ea-ad61c05bef5b", 4 | "executors": [{}, {}], 5 | "icon": "computer-x.png", 6 | "idle": true, 7 | "jnlpAgent": true, 8 | "launchSupported": false, 9 | "loadStatistics": {}, 10 | "manualLaunchAllowed": true, 11 | "monitorData": { 12 | "hudson.node_monitors.SwapSpaceMonitor": null, 13 | "hudson.node_monitors.ArchitectureMonitor": null, 14 | "hudson.node_monitors.ResponseTimeMonitor": { 15 | "average": 5000 16 | }, 17 | "hudson.node_monitors.TemporarySpaceMonitor": null, 18 | "hudson.node_monitors.DiskSpaceMonitor": null, 19 | "hudson.node_monitors.ClockMonitor": null 20 | }, 21 | "numExecutors": 2, 22 | "offline": true, 23 | "offlineCause": {}, 24 | "offlineCauseReason": "update", 25 | "oneOffExecutors": [], 26 | "temporarilyOffline": true 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/nodeList.json: -------------------------------------------------------------------------------- 1 | { 2 | "busyExecutors": 0, 3 | "computer": [ 4 | { 5 | "actions": [], 6 | "displayName": "master", 7 | "executors": [{}, {}, {}], 8 | "icon": "computer.png", 9 | "idle": true, 10 | "jnlpAgent": false, 11 | "launchSupported": true, 12 | "loadStatistics": {}, 13 | "manualLaunchAllowed": true, 14 | "monitorData": { 15 | "hudson.node_monitors.SwapSpaceMonitor": { 16 | "availablePhysicalMemory": 128483328, 17 | "availableSwapSpace": 805302272, 18 | "totalPhysicalMemory": 515358720, 19 | "totalSwapSpace": 805302272 20 | }, 21 | "hudson.node_monitors.ArchitectureMonitor": "Linux (amd64)", 22 | "hudson.node_monitors.ResponseTimeMonitor": { 23 | "average": 0 24 | }, 25 | "hudson.node_monitors.TemporarySpaceMonitor": { 26 | "path": "/tmp", 27 | "size": 77652922368 28 | }, 29 | "hudson.node_monitors.DiskSpaceMonitor": { 30 | "path": "/var/lib/jenkins", 31 | "size": 77652922368 32 | }, 33 | "hudson.node_monitors.ClockMonitor": { 34 | "diff": 0 35 | } 36 | }, 37 | "numExecutors": 3, 38 | "offline": false, 39 | "offlineCause": null, 40 | "offlineCauseReason": "", 41 | "oneOffExecutors": [], 42 | "temporarilyOffline": false 43 | }, 44 | { 45 | "actions": [], 46 | "displayName": "test", 47 | "executors": [{}], 48 | "icon": "computer-x.png", 49 | "idle": true, 50 | "jnlpAgent": false, 51 | "launchSupported": true, 52 | "loadStatistics": {}, 53 | "manualLaunchAllowed": true, 54 | "monitorData": { 55 | "hudson.node_monitors.SwapSpaceMonitor": null, 56 | "hudson.node_monitors.ArchitectureMonitor": null, 57 | "hudson.node_monitors.ResponseTimeMonitor": { 58 | "average": 5000 59 | }, 60 | "hudson.node_monitors.TemporarySpaceMonitor": null, 61 | "hudson.node_monitors.DiskSpaceMonitor": null, 62 | "hudson.node_monitors.ClockMonitor": null 63 | }, 64 | "numExecutors": 1, 65 | "offline": true, 66 | "offlineCause": {}, 67 | "offlineCauseReason": "Time out for last 5 try", 68 | "oneOffExecutors": [], 69 | "temporarilyOffline": true 70 | }, 71 | { 72 | "actions": [], 73 | "displayName": "test-node-abc00edf-4e7e-4dfe-a6d6-157d19d592bc", 74 | "executors": [{}, {}], 75 | "icon": "computer-x.png", 76 | "idle": true, 77 | "jnlpAgent": true, 78 | "launchSupported": false, 79 | "loadStatistics": {}, 80 | "manualLaunchAllowed": true, 81 | "monitorData": { 82 | "hudson.node_monitors.SwapSpaceMonitor": null, 83 | "hudson.node_monitors.ArchitectureMonitor": null, 84 | "hudson.node_monitors.ResponseTimeMonitor": null, 85 | "hudson.node_monitors.TemporarySpaceMonitor": null, 86 | "hudson.node_monitors.DiskSpaceMonitor": null, 87 | "hudson.node_monitors.ClockMonitor": null 88 | }, 89 | "numExecutors": 2, 90 | "offline": true, 91 | "offlineCause": null, 92 | "offlineCauseReason": "", 93 | "oneOffExecutors": [], 94 | "temporarilyOffline": false 95 | } 96 | ], 97 | "displayName": "nodes", 98 | "totalExecutors": 3 99 | } 100 | -------------------------------------------------------------------------------- /test/fixtures/pluginList.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.LocalPluginManager", 3 | "plugins": [ 4 | { 5 | "active": true, 6 | "backupVersion": null, 7 | "bundled": false, 8 | "deleted": false, 9 | "dependencies": [ 10 | { 11 | "optional": false, 12 | "shortName": "junit", 13 | "version": "1.2" 14 | }, 15 | { 16 | "optional": false, 17 | "shortName": "script-security", 18 | "version": "1.13" 19 | } 20 | ], 21 | "downgradable": false, 22 | "enabled": true, 23 | "hasUpdate": true, 24 | "longName": "Matrix Project Plugin", 25 | "pinned": false, 26 | "shortName": "matrix-project", 27 | "supportsDynamicLoad": "MAYBE", 28 | "url": "https://wiki.jenkins-ci.org/display/JENKINS/Matrix+Project+Plugin", 29 | "version": "1.7.1" 30 | }, 31 | { 32 | "active": true, 33 | "backupVersion": null, 34 | "bundled": false, 35 | "deleted": false, 36 | "dependencies": [ 37 | { 38 | "optional": false, 39 | "shortName": "structs", 40 | "version": "1.2" 41 | } 42 | ], 43 | "downgradable": false, 44 | "enabled": true, 45 | "hasUpdate": true, 46 | "longName": "JUnit Plugin", 47 | "pinned": false, 48 | "shortName": "junit", 49 | "supportsDynamicLoad": "MAYBE", 50 | "url": "http://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin", 51 | "version": "1.19" 52 | }, 53 | { 54 | "active": true, 55 | "backupVersion": null, 56 | "bundled": false, 57 | "deleted": false, 58 | "dependencies": [], 59 | "downgradable": false, 60 | "enabled": true, 61 | "hasUpdate": true, 62 | "longName": "Structs Plugin", 63 | "pinned": false, 64 | "shortName": "structs", 65 | "supportsDynamicLoad": "MAYBE", 66 | "url": "https://wiki.jenkins-ci.org/display/JENKINS/Structs+plugin", 67 | "version": "1.5" 68 | }, 69 | { 70 | "active": true, 71 | "backupVersion": null, 72 | "bundled": false, 73 | "deleted": false, 74 | "dependencies": [], 75 | "downgradable": false, 76 | "enabled": true, 77 | "hasUpdate": true, 78 | "longName": "Script Security Plugin", 79 | "pinned": false, 80 | "shortName": "script-security", 81 | "supportsDynamicLoad": "MAYBE", 82 | "url": "https://wiki.jenkins-ci.org/display/JENKINS/Script+Security+Plugin", 83 | "version": "1.27" 84 | }, 85 | { 86 | "active": true, 87 | "backupVersion": null, 88 | "bundled": false, 89 | "deleted": false, 90 | "dependencies": [ 91 | { 92 | "optional": false, 93 | "shortName": "javadoc", 94 | "version": "1.0" 95 | }, 96 | { 97 | "optional": false, 98 | "shortName": "junit", 99 | "version": "1.6" 100 | }, 101 | { 102 | "optional": false, 103 | "shortName": "mailer", 104 | "version": "1.7" 105 | }, 106 | { 107 | "optional": true, 108 | "shortName": "token-macro", 109 | "version": "1.1;resolution:=optional" 110 | } 111 | ], 112 | "downgradable": false, 113 | "enabled": true, 114 | "hasUpdate": true, 115 | "longName": "Maven Integration plugin", 116 | "pinned": false, 117 | "shortName": "maven-plugin", 118 | "supportsDynamicLoad": "MAYBE", 119 | "url": "http://wiki.jenkins-ci.org/display/JENKINS/Maven+Project+Plugin", 120 | "version": "2.14" 121 | }, 122 | { 123 | "active": true, 124 | "backupVersion": null, 125 | "bundled": false, 126 | "deleted": false, 127 | "dependencies": [], 128 | "downgradable": false, 129 | "enabled": true, 130 | "hasUpdate": true, 131 | "longName": "Javadoc Plugin", 132 | "pinned": false, 133 | "shortName": "javadoc", 134 | "supportsDynamicLoad": "MAYBE", 135 | "url": "http://wiki.jenkins-ci.org/display/JENKINS/Javadoc+Plugin", 136 | "version": "1.4" 137 | }, 138 | { 139 | "active": true, 140 | "backupVersion": null, 141 | "bundled": false, 142 | "deleted": false, 143 | "dependencies": [ 144 | { 145 | "optional": false, 146 | "shortName": "display-url-api", 147 | "version": "0.2" 148 | } 149 | ], 150 | "downgradable": false, 151 | "enabled": true, 152 | "hasUpdate": true, 153 | "longName": "Jenkins Mailer Plugin", 154 | "pinned": false, 155 | "shortName": "mailer", 156 | "supportsDynamicLoad": "MAYBE", 157 | "url": "http://wiki.jenkins-ci.org/display/JENKINS/Mailer", 158 | "version": "1.18" 159 | }, 160 | { 161 | "active": true, 162 | "backupVersion": null, 163 | "bundled": false, 164 | "deleted": false, 165 | "dependencies": [ 166 | { 167 | "optional": false, 168 | "shortName": "junit", 169 | "version": "1.3" 170 | } 171 | ], 172 | "downgradable": false, 173 | "enabled": true, 174 | "hasUpdate": true, 175 | "longName": "Display URL API", 176 | "pinned": false, 177 | "shortName": "display-url-api", 178 | "supportsDynamicLoad": "YES", 179 | "url": "https://wiki.jenkins-ci.org/display/JENKINS/Display+URL+API+Plugin", 180 | "version": "0.5" 181 | }, 182 | { 183 | "active": true, 184 | "backupVersion": null, 185 | "bundled": false, 186 | "deleted": false, 187 | "dependencies": [], 188 | "downgradable": false, 189 | "enabled": true, 190 | "hasUpdate": true, 191 | "longName": "Token Macro Plugin", 192 | "pinned": false, 193 | "shortName": "token-macro", 194 | "supportsDynamicLoad": "MAYBE", 195 | "url": "http://wiki.jenkins-ci.org/display/JENKINS/Token+Macro+Plugin", 196 | "version": "2.0" 197 | }, 198 | { 199 | "active": true, 200 | "backupVersion": null, 201 | "bundled": false, 202 | "deleted": false, 203 | "dependencies": [], 204 | "downgradable": false, 205 | "enabled": true, 206 | "hasUpdate": false, 207 | "longName": "JavaScript GUI Lib: ACE Editor bundle plugin", 208 | "pinned": false, 209 | "shortName": "ace-editor", 210 | "supportsDynamicLoad": "YES", 211 | "url": "https://wiki.jenkins-ci.org/display/JENKINS/ACE+Editor+Plugin", 212 | "version": "1.1" 213 | }, 214 | { 215 | "active": true, 216 | "backupVersion": null, 217 | "bundled": false, 218 | "deleted": false, 219 | "dependencies": [ 220 | { 221 | "optional": false, 222 | "shortName": "matrix-project", 223 | "version": "1.2.1" 224 | }, 225 | { 226 | "optional": false, 227 | "shortName": "analysis-core", 228 | "version": "1.80" 229 | }, 230 | { 231 | "optional": false, 232 | "shortName": "maven-plugin", 233 | "version": "2.9" 234 | }, 235 | { 236 | "optional": true, 237 | "shortName": "tasks", 238 | "version": "4.46;resolution:=optional" 239 | }, 240 | { 241 | "optional": true, 242 | "shortName": "warnings", 243 | "version": "4.49;resolution:=optional" 244 | }, 245 | { 246 | "optional": true, 247 | "shortName": "dashboard-view", 248 | "version": "2.9.4;resolution:=optional" 249 | }, 250 | { 251 | "optional": true, 252 | "shortName": "findbugs", 253 | "version": "4.62;resolution:=optional" 254 | }, 255 | { 256 | "optional": true, 257 | "shortName": "dry", 258 | "version": "2.42;resolution:=optional" 259 | }, 260 | { 261 | "optional": true, 262 | "shortName": "pmd", 263 | "version": "3.42;resolution:=optional" 264 | }, 265 | { 266 | "optional": true, 267 | "shortName": "checkstyle", 268 | "version": "3.43;resolution:=optional" 269 | }, 270 | { 271 | "optional": true, 272 | "shortName": "token-macro", 273 | "version": "1.10;resolution:=optional" 274 | } 275 | ], 276 | "downgradable": false, 277 | "enabled": true, 278 | "hasUpdate": true, 279 | "longName": "Static Analysis Collector Plug-in", 280 | "pinned": false, 281 | "shortName": "analysis-collector", 282 | "supportsDynamicLoad": "MAYBE", 283 | "url": "http://wiki.jenkins-ci.org/x/tgeIAg", 284 | "version": "1.50" 285 | }, 286 | { 287 | "active": true, 288 | "backupVersion": null, 289 | "bundled": false, 290 | "deleted": false, 291 | "dependencies": [ 292 | { 293 | "optional": false, 294 | "shortName": "matrix-project", 295 | "version": "1.2.1" 296 | }, 297 | { 298 | "optional": false, 299 | "shortName": "maven-plugin", 300 | "version": "2.9" 301 | }, 302 | { 303 | "optional": false, 304 | "shortName": "analysis-core", 305 | "version": "1.80" 306 | }, 307 | { 308 | "optional": true, 309 | "shortName": "dashboard-view", 310 | "version": "2.9.4;resolution:=optional" 311 | }, 312 | { 313 | "optional": true, 314 | "shortName": "token-macro", 315 | "version": "1.10;resolution:=optional" 316 | } 317 | ], 318 | "downgradable": false, 319 | "enabled": true, 320 | "hasUpdate": true, 321 | "longName": "Task Scanner Plug-in", 322 | "pinned": false, 323 | "shortName": "tasks", 324 | "supportsDynamicLoad": "MAYBE", 325 | "url": "http://wiki.jenkins-ci.org/x/XoAs", 326 | "version": "4.50" 327 | }, 328 | { 329 | "active": true, 330 | "backupVersion": null, 331 | "bundled": false, 332 | "deleted": false, 333 | "dependencies": [ 334 | { 335 | "optional": false, 336 | "shortName": "maven-plugin", 337 | "version": "2.3" 338 | }, 339 | { 340 | "optional": false, 341 | "shortName": "matrix-project", 342 | "version": "1.0" 343 | }, 344 | { 345 | "optional": false, 346 | "shortName": "junit", 347 | "version": "1.0" 348 | } 349 | ], 350 | "downgradable": false, 351 | "enabled": true, 352 | "hasUpdate": true, 353 | "longName": "Dashboard View", 354 | "pinned": false, 355 | "shortName": "dashboard-view", 356 | "supportsDynamicLoad": "MAYBE", 357 | "url": "http://wiki.jenkins-ci.org/display/JENKINS/Dashboard+View", 358 | "version": "2.9.10" 359 | }, 360 | { 361 | "active": true, 362 | "backupVersion": null, 363 | "bundled": false, 364 | "deleted": false, 365 | "dependencies": [ 366 | { 367 | "optional": false, 368 | "shortName": "matrix-project", 369 | "version": "1.4" 370 | }, 371 | { 372 | "optional": false, 373 | "shortName": "maven-plugin", 374 | "version": "2.9" 375 | }, 376 | { 377 | "optional": true, 378 | "shortName": "ant", 379 | "version": "1.1;resolution:=optional" 380 | }, 381 | { 382 | "optional": true, 383 | "shortName": "token-macro", 384 | "version": "1.10;resolution:=optional" 385 | }, 386 | { 387 | "optional": true, 388 | "shortName": "dashboard-view", 389 | "version": "2.9.4;resolution:=optional" 390 | } 391 | ], 392 | "downgradable": false, 393 | "enabled": true, 394 | "hasUpdate": true, 395 | "longName": "Static Analysis Utilities", 396 | "pinned": false, 397 | "shortName": "analysis-core", 398 | "supportsDynamicLoad": "MAYBE", 399 | "url": "http://wiki.jenkins-ci.org/x/CwDgAQ", 400 | "version": "1.82" 401 | }, 402 | { 403 | "active": true, 404 | "backupVersion": null, 405 | "bundled": false, 406 | "deleted": false, 407 | "dependencies": [ 408 | { 409 | "optional": false, 410 | "shortName": "structs", 411 | "version": "1.3" 412 | } 413 | ], 414 | "downgradable": false, 415 | "enabled": true, 416 | "hasUpdate": true, 417 | "longName": "Ant Plugin", 418 | "pinned": false, 419 | "shortName": "ant", 420 | "supportsDynamicLoad": "MAYBE", 421 | "url": "https://wiki.jenkins-ci.org/display/JENKINS/Ant+Plugin", 422 | "version": "1.4" 423 | }, 424 | { 425 | "active": true, 426 | "backupVersion": null, 427 | "bundled": false, 428 | "deleted": false, 429 | "dependencies": [ 430 | { 431 | "optional": false, 432 | "shortName": "matrix-project", 433 | "version": "1.2.1" 434 | }, 435 | { 436 | "optional": false, 437 | "shortName": "analysis-core", 438 | "version": "1.82" 439 | }, 440 | { 441 | "optional": false, 442 | "shortName": "script-security", 443 | "version": "1.17" 444 | }, 445 | { 446 | "optional": false, 447 | "shortName": "maven-plugin", 448 | "version": "2.9" 449 | }, 450 | { 451 | "optional": true, 452 | "shortName": "dashboard-view", 453 | "version": "2.9.4;resolution:=optional" 454 | }, 455 | { 456 | "optional": true, 457 | "shortName": "token-macro", 458 | "version": "1.10;resolution:=optional" 459 | }, 460 | { 461 | "optional": true, 462 | "shortName": "violations", 463 | "version": "0.7.11;resolution:=optional" 464 | } 465 | ], 466 | "downgradable": false, 467 | "enabled": true, 468 | "hasUpdate": true, 469 | "longName": "Warnings Plug-in", 470 | "pinned": false, 471 | "shortName": "warnings", 472 | "supportsDynamicLoad": "MAYBE", 473 | "url": "http://wiki.jenkins-ci.org/x/G4CGAQ", 474 | "version": "4.59" 475 | }, 476 | { 477 | "active": true, 478 | "backupVersion": null, 479 | "bundled": false, 480 | "deleted": false, 481 | "dependencies": [ 482 | { 483 | "optional": false, 484 | "shortName": "maven-plugin", 485 | "version": "1.399" 486 | }, 487 | { 488 | "optional": false, 489 | "shortName": "ant", 490 | "version": "1.0" 491 | }, 492 | { 493 | "optional": false, 494 | "shortName": "javadoc", 495 | "version": "1.0" 496 | }, 497 | { 498 | "optional": false, 499 | "shortName": "external-monitor-job", 500 | "version": "1.0" 501 | }, 502 | { 503 | "optional": false, 504 | "shortName": "ldap", 505 | "version": "1.0" 506 | }, 507 | { 508 | "optional": false, 509 | "shortName": "pam-auth", 510 | "version": "1.0" 511 | }, 512 | { 513 | "optional": false, 514 | "shortName": "mailer", 515 | "version": "1.2" 516 | }, 517 | { 518 | "optional": false, 519 | "shortName": "matrix-auth", 520 | "version": "1.0.2" 521 | }, 522 | { 523 | "optional": false, 524 | "shortName": "windows-slaves", 525 | "version": "1.0" 526 | }, 527 | { 528 | "optional": false, 529 | "shortName": "antisamy-markup-formatter", 530 | "version": "1.0" 531 | }, 532 | { 533 | "optional": false, 534 | "shortName": "matrix-project", 535 | "version": "1.0" 536 | }, 537 | { 538 | "optional": false, 539 | "shortName": "junit", 540 | "version": "1.0" 541 | } 542 | ], 543 | "downgradable": false, 544 | "enabled": true, 545 | "hasUpdate": false, 546 | "longName": "Jenkins Violations plugin", 547 | "pinned": false, 548 | "shortName": "violations", 549 | "supportsDynamicLoad": "MAYBE", 550 | "url": "wiki.jenkins-ci.org/display/JENKINS/Violations", 551 | "version": "0.7.11" 552 | }, 553 | { 554 | "active": true, 555 | "backupVersion": null, 556 | "bundled": false, 557 | "deleted": false, 558 | "dependencies": [ 559 | { 560 | "optional": false, 561 | "shortName": "analysis-core", 562 | "version": "1.80" 563 | }, 564 | { 565 | "optional": false, 566 | "shortName": "matrix-project", 567 | "version": "1.2.1" 568 | }, 569 | { 570 | "optional": false, 571 | "shortName": "maven-plugin", 572 | "version": "2.9" 573 | }, 574 | { 575 | "optional": true, 576 | "shortName": "token-macro", 577 | "version": "1.10;resolution:=optional" 578 | }, 579 | { 580 | "optional": true, 581 | "shortName": "dashboard-view", 582 | "version": "2.9.4;resolution:=optional" 583 | } 584 | ], 585 | "downgradable": false, 586 | "enabled": true, 587 | "hasUpdate": true, 588 | "longName": "FindBugs Plug-in", 589 | "pinned": false, 590 | "shortName": "findbugs", 591 | "supportsDynamicLoad": "MAYBE", 592 | "url": "http://wiki.jenkins-ci.org/x/GYAs", 593 | "version": "4.69" 594 | }, 595 | { 596 | "active": true, 597 | "backupVersion": null, 598 | "bundled": false, 599 | "deleted": false, 600 | "dependencies": [ 601 | { 602 | "optional": false, 603 | "shortName": "analysis-core", 604 | "version": "1.80" 605 | }, 606 | { 607 | "optional": false, 608 | "shortName": "matrix-project", 609 | "version": "1.2.1" 610 | }, 611 | { 612 | "optional": false, 613 | "shortName": "maven-plugin", 614 | "version": "2.9" 615 | }, 616 | { 617 | "optional": true, 618 | "shortName": "dashboard-view", 619 | "version": "2.9.4;resolution:=optional" 620 | }, 621 | { 622 | "optional": true, 623 | "shortName": "token-macro", 624 | "version": "1.10;resolution:=optional" 625 | } 626 | ], 627 | "downgradable": false, 628 | "enabled": true, 629 | "hasUpdate": true, 630 | "longName": "DRY Plug-in", 631 | "pinned": false, 632 | "shortName": "dry", 633 | "supportsDynamicLoad": "MAYBE", 634 | "url": "http://wiki.jenkins-ci.org/x/X4IuAg", 635 | "version": "2.46" 636 | }, 637 | { 638 | "active": true, 639 | "backupVersion": null, 640 | "bundled": false, 641 | "deleted": false, 642 | "dependencies": [ 643 | { 644 | "optional": false, 645 | "shortName": "maven-plugin", 646 | "version": "2.9" 647 | }, 648 | { 649 | "optional": false, 650 | "shortName": "matrix-project", 651 | "version": "1.2.1" 652 | }, 653 | { 654 | "optional": false, 655 | "shortName": "analysis-core", 656 | "version": "1.80" 657 | }, 658 | { 659 | "optional": true, 660 | "shortName": "token-macro", 661 | "version": "1.10;resolution:=optional" 662 | }, 663 | { 664 | "optional": true, 665 | "shortName": "dashboard-view", 666 | "version": "2.9.4;resolution:=optional" 667 | } 668 | ], 669 | "downgradable": false, 670 | "enabled": true, 671 | "hasUpdate": true, 672 | "longName": "PMD Plug-in", 673 | "pinned": false, 674 | "shortName": "pmd", 675 | "supportsDynamicLoad": "MAYBE", 676 | "url": "http://wiki.jenkins-ci.org/x/GAAHAQ", 677 | "version": "3.46" 678 | }, 679 | { 680 | "active": true, 681 | "backupVersion": null, 682 | "bundled": false, 683 | "deleted": false, 684 | "dependencies": [ 685 | { 686 | "optional": false, 687 | "shortName": "matrix-project", 688 | "version": "1.2.1" 689 | }, 690 | { 691 | "optional": false, 692 | "shortName": "analysis-core", 693 | "version": "1.80" 694 | }, 695 | { 696 | "optional": false, 697 | "shortName": "maven-plugin", 698 | "version": "2.9" 699 | }, 700 | { 701 | "optional": true, 702 | "shortName": "dashboard-view", 703 | "version": "2.9.4;resolution:=optional" 704 | }, 705 | { 706 | "optional": true, 707 | "shortName": "token-macro", 708 | "version": "1.10;resolution:=optional" 709 | } 710 | ], 711 | "downgradable": false, 712 | "enabled": true, 713 | "hasUpdate": true, 714 | "longName": "Checkstyle Plug-in", 715 | "pinned": false, 716 | "shortName": "checkstyle", 717 | "supportsDynamicLoad": "MAYBE", 718 | "url": "http://wiki.jenkins-ci.org/x/GYCGAQ", 719 | "version": "3.47" 720 | } 721 | ] 722 | } 723 | -------------------------------------------------------------------------------- /test/fixtures/queueItem.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [ 3 | { 4 | "causes": [ 5 | { 6 | "shortDescription": "Started by user anonymous", 7 | "userId": null, 8 | "userName": "anonymous" 9 | } 10 | ] 11 | } 12 | ], 13 | "blocked": false, 14 | "buildable": false, 15 | "id": 130, 16 | "inQueueSince": 1406363479853, 17 | "params": "", 18 | "stuck": false, 19 | "task": { 20 | "name": "test-job-b7ef0845-6515-444c-96a1-d2266d5e0f18", 21 | "url": "http://localhost:8080/job/test-job-b7ef0845-6515-444c-96a1-d2266d5e0f18/", 22 | "color": "blue" 23 | }, 24 | "url": "queue/item/130/", 25 | "why": null, 26 | "executable": { 27 | "number": 28, 28 | "url": "http://localhost:8080/job/test-job-b7ef0845-6515-444c-96a1-d2266d5e0f18/28/" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/fixtures/queueList.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "actions": [ 5 | { 6 | "causes": [ 7 | { 8 | "shortDescription": "Started by user anonymous", 9 | "userId": null, 10 | "userName": "anonymous" 11 | } 12 | ] 13 | } 14 | ], 15 | "blocked": false, 16 | "buildable": true, 17 | "id": 130, 18 | "inQueueSince": 1406363479853, 19 | "params": "", 20 | "stuck": false, 21 | "task": { 22 | "name": "test-job-b7ef0845-6515-444c-96a1-d2266d5e0f18", 23 | "url": "http://localhost:8080/job/test-job-b7ef0845-6515-444c-96a1-d2266d5e0f18/", 24 | "color": "notbuilt" 25 | }, 26 | "url": "queue/item/130/", 27 | "why": "Waiting for next available executor", 28 | "buildableStartMilliseconds": 1406363479854, 29 | "pending": true 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /test/fixtures/viewConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | test-view 4 | false 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | false 21 | 22 | -------------------------------------------------------------------------------- /test/fixtures/viewCreate.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-view", 3 | "mode": "hudson.model.ListView", 4 | "json": "{\"name\":\"test-view\",\"mode\":\"hudson.model.ListView\"}" 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/viewGet.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": null, 3 | "jobs": [ 4 | { 5 | "name": "test", 6 | "url": "http://localhost:8080/job/test/", 7 | "color": "blue" 8 | } 9 | ], 10 | "name": "test", 11 | "property": [], 12 | "url": "http://localhost:8080/view/test-view/" 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/viewGetListView.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "just a test", 3 | "jobs": [ 4 | { 5 | "name": "test", 6 | "url": "http://localhost:8080/job/test/", 7 | "color": "blue" 8 | } 9 | ], 10 | "name": "test", 11 | "property": [], 12 | "url": "http://localhost:8080/view/test-view/" 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/viewList.json: -------------------------------------------------------------------------------- 1 | { 2 | "views": [ 3 | { 4 | "url": "http://localhost:8080/", 5 | "name": "All" 6 | }, 7 | { 8 | "url": "http://localhost:8080/view/test/", 9 | "name": "Test" 10 | } 11 | ], 12 | "useSecurity": false, 13 | "useCrumbs": false, 14 | "unlabeledLoad": {}, 15 | "slaveAgentPort": 0, 16 | "quietingDown": false, 17 | "primaryView": { 18 | "url": "http://localhost:8080/", 19 | "name": "All" 20 | }, 21 | "assignedLabels": [{}], 22 | "mode": "NORMAL", 23 | "nodeDescription": "the master Jenkins node", 24 | "nodeName": "", 25 | "numExecutors": 2, 26 | "description": null, 27 | "jobs": [ 28 | { 29 | "color": "blue", 30 | "url": "http://localhost:8080/job/asdf/", 31 | "name": "asdf" 32 | }, 33 | { 34 | "color": "notbuilt", 35 | "url": "http://localhost:8080/job/test-e474800e-9d04-437a-8dd3-c29c7156185e/", 36 | "name": "test-e474800e-9d04-437a-8dd3-c29c7156185e" 37 | } 38 | ], 39 | "overallLoad": {} 40 | } 41 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | const auto = require("async/auto"); 2 | const fixtures = require("fixturefiles"); 3 | const nock = require("nock"); 4 | const uuid = require("node-uuid"); 5 | 6 | const NOCK_REC = process.env.NOCK_REC === "true"; 7 | const NOCK_OFF = process.env.NOCK_OFF === "true" || NOCK_REC; 8 | 9 | const URL = process.env.JENKINS_TEST_URL || "http://localhost:8080"; 10 | const CRUMB_ISSUER = NOCK_OFF && process.env.CRUMB_ISSUER !== "false"; 11 | 12 | async function setup(opts) { 13 | const test = opts.test; 14 | const jenkins = test.jenkins; 15 | 16 | const unique = (name) => { 17 | if (NOCK_OFF) { 18 | name += "-" + uuid.v4(); 19 | } 20 | return `test-${name}`; 21 | }; 22 | 23 | test.jobName = unique("job"); 24 | test.nodeName = unique("node"); 25 | test.viewName = unique("view"); 26 | test.folderName = unique("folder"); 27 | 28 | if (!NOCK_OFF) { 29 | nock.disableNetConnect(); 30 | return; 31 | } 32 | 33 | const promises = []; 34 | 35 | if (opts.job) { 36 | promises.push(jenkins.job.create(test.jobName, fixtures.jobCreate)); 37 | } 38 | 39 | if (opts.folder) { 40 | promises.push(jenkins.job.create(test.folderName, fixtures.folderCreate)); 41 | } 42 | 43 | if (opts.node) { 44 | promises.push( 45 | jenkins.node.create({ 46 | name: test.nodeName, 47 | launcher: { 48 | "stapler-class": "hudson.slaves.CommandLauncher", 49 | command: "java -jar /usr/share/jenkins/ref/slave.jar", 50 | }, 51 | }) 52 | ); 53 | } 54 | 55 | if (opts.view) { 56 | promises.push(jenkins.view.create(test.viewName)); 57 | } 58 | 59 | if (NOCK_REC) nock.recorder.rec(); 60 | 61 | return Promise.all(promises); 62 | } 63 | 64 | async function teardown(opts) { 65 | if (NOCK_REC) nock.restore(); 66 | } 67 | 68 | async function cleanup(opts) { 69 | const test = opts.test; 70 | 71 | if (!NOCK_OFF) { 72 | nock.enableNetConnect(); 73 | return; 74 | } 75 | 76 | const jobs = {}; 77 | 78 | jobs.listJobs = async () => { 79 | return test.jenkins.job.list(); 80 | }; 81 | 82 | jobs.listNodes = async () => { 83 | return test.jenkins.node.list(); 84 | }; 85 | 86 | jobs.listViews = async () => { 87 | return test.jenkins.view.list(); 88 | }; 89 | 90 | jobs.listSystemCredentials = async () => { 91 | return test.jenkins.credentials.list("manage", "system", "_"); 92 | }; 93 | 94 | jobs.destroyJobs = [ 95 | "listJobs", 96 | async (results) => { 97 | return Promise.all( 98 | results.listJobs 99 | .map((job) => job.name) 100 | .filter((name) => name.match(/^test-job-/)) 101 | .map((name) => test.jenkins.job.destroy(name)) 102 | ); 103 | }, 104 | ]; 105 | 106 | jobs.destroyNodes = [ 107 | "listNodes", 108 | async (results) => { 109 | return Promise.all( 110 | results.listNodes 111 | .map((node) => node.displayName) 112 | .filter((name) => name.match(/^test-node-/)) 113 | .map((name) => test.jenkins.node.destroy(name)) 114 | ); 115 | }, 116 | ]; 117 | 118 | jobs.destroyViews = [ 119 | "listViews", 120 | async (results) => { 121 | return Promise.all( 122 | results.listViews 123 | .map((node) => node.name) 124 | .filter((name) => name.match(/^test-view-/)) 125 | .map((name) => test.jenkins.view.destroy(name)) 126 | ); 127 | }, 128 | ]; 129 | 130 | jobs.destroyFolders = [ 131 | "listJobs", 132 | async (results) => { 133 | return Promise.all( 134 | results.listJobs 135 | .map((job) => job.name) 136 | .filter((name) => name.match(/^test-folder-/)) 137 | .map((name) => test.jenkins.job.destroy(name)) 138 | ); 139 | }, 140 | ]; 141 | 142 | jobs.destroySystemCredentials = [ 143 | "listSystemCredentials", 144 | async (results) => { 145 | return Promise.all( 146 | results.listSystemCredentials 147 | .map((credential) => credential.id) 148 | .filter((id) => id.match(/^user-new-cred/)) 149 | .map((id) => 150 | test.jenkins.credentials.destroy(id, "manage", "system", "_") 151 | ) 152 | ); 153 | }, 154 | ]; 155 | 156 | return auto(jobs); 157 | } 158 | 159 | exports.cleanup = cleanup; 160 | exports.config = { url: URL, crumbIssuer: CRUMB_ISSUER }; 161 | exports.nock = { on: !NOCK_OFF, off: NOCK_OFF }; 162 | exports.ndescribe = NOCK_OFF ? describe.skip : describe; 163 | exports.nit = NOCK_OFF ? it.skip : it; 164 | exports.setup = setup; 165 | exports.teardown = teardown; 166 | -------------------------------------------------------------------------------- /test/jenkins.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const retry = require("async/retry"); 4 | const debug = require("debug"); 5 | const fixtures = require("fixturefiles"); 6 | const nock = require("nock"); 7 | const papi = require("papi"); 8 | const should = require("should"); 9 | const sinon = require("sinon"); 10 | const uuid = require("node-uuid"); 11 | 12 | const helper = require("./helper"); 13 | const Jenkins = require("../lib"); 14 | 15 | const ndescribe = helper.ndescribe; 16 | const nit = helper.nit; 17 | 18 | describe("jenkins", function () { 19 | beforeEach(function () { 20 | this.sinon = sinon.createSandbox(); 21 | 22 | this.url = helper.config.url; 23 | this.nock = nock(this.url); 24 | this.jenkins = new Jenkins({ 25 | baseUrl: this.url, 26 | crumbIssuer: helper.config.crumbIssuer, 27 | }); 28 | this.jenkins.on("log", debug("jenkins:client")); 29 | }); 30 | 31 | afterEach(async function () { 32 | nock.cleanAll(); 33 | 34 | this.sinon.restore(); 35 | 36 | return helper.teardown({ test: this }); 37 | }); 38 | 39 | after(async function () { 40 | return helper.cleanup({ test: this }); 41 | }); 42 | 43 | describe("build", function () { 44 | beforeEach(async function () { 45 | return helper.setup({ job: true, test: this }); 46 | }); 47 | 48 | describe("get", function () { 49 | it("should return build details", async function () { 50 | this.nock 51 | .post(`/job/${this.jobName}/build`) 52 | .reply(201, "", { location: "http://localhost:8080/queue/item/1/" }) 53 | .get(`/job/${this.jobName}/1/api/json`) 54 | .reply(200, fixtures.buildGet); 55 | 56 | await this.jenkins.job.build(this.jobName); 57 | 58 | const data = await retry(100, async () => { 59 | return this.jenkins.build.get(this.jobName, 1); 60 | }); 61 | 62 | should(data).have.property("number"); 63 | should(data.number).equal(1); 64 | }); 65 | 66 | it("should return build log", async function () { 67 | this.nock 68 | .post(`/job/${this.jobName}/build`) 69 | .reply(201, "", { location: "http://localhost:8080/queue/item/1/" }) 70 | .post(`/job/${this.jobName}/1/logText/progressiveText`) 71 | .reply(200, fixtures.consoleText, { 72 | "Content-Type": "text/plain;charset=UTF-8", 73 | }); 74 | 75 | await this.jenkins.job.build(this.jobName); 76 | 77 | await retry(100, async () => { 78 | return this.jenkins.build.log(this.jobName, 1); 79 | }); 80 | }); 81 | 82 | nit("should get with options", async function () { 83 | this.nock 84 | .get("/job/test/1/api/json?tree=%5B*%5B*%5D%5D") 85 | .reply(200, fixtures.buildGet); 86 | 87 | const data = await this.jenkins.build.get("test", 1, { 88 | tree: "[*[*]]", 89 | }); 90 | should(data).have.property("number"); 91 | }); 92 | 93 | nit("should return error when it does not exist", async function () { 94 | this.nock.get("/job/test/2/api/json").reply(404); 95 | 96 | await shouldThrow(async () => { 97 | await this.jenkins.build.get("test", 2); 98 | }, "jenkins: build.get: test 2 not found"); 99 | }); 100 | }); 101 | 102 | describe("stop", function () { 103 | it("should stop build", async function () { 104 | this.nock 105 | .post(`/job/${this.jobName}/build`) 106 | .reply(201, "", { location: "http://localhost:8080/queue/item/1/" }) 107 | .post(`/job/${this.jobName}/1/stop`) 108 | .reply(302); 109 | 110 | await this.jenkins.job.build(this.jobName); 111 | 112 | await retry(100, async () => { 113 | return this.jenkins.build.stop(this.jobName, 1); 114 | }); 115 | }); 116 | }); 117 | 118 | describe("term", function () { 119 | nit("should terminate build", async function () { 120 | this.nock 121 | .post(`/job/${this.jobName}/build`) 122 | .reply(201, "", { location: "http://localhost:8080/queue/item/1/" }) 123 | .post(`/job/${this.jobName}/1/term`) 124 | .reply(200); 125 | 126 | await this.jenkins.job.build(this.jobName); 127 | 128 | await retry(100, async () => { 129 | await this.jenkins.build.term(this.jobName, 1); 130 | }); 131 | }); 132 | }); 133 | }); 134 | 135 | describe("job", function () { 136 | beforeEach(async function () { 137 | return helper.setup({ job: true, test: this }); 138 | }); 139 | 140 | describe("build", function () { 141 | it("should start build", async function () { 142 | this.nock 143 | .post(`/job/${this.jobName}/build`) 144 | .reply(201, "", { location: "http://localhost:8080/queue/item/5/" }); 145 | 146 | const number = await this.jenkins.job.build(this.jobName); 147 | should(number).be.type("number"); 148 | should(number).be.above(0); 149 | }); 150 | 151 | it("should not error on 302", async function () { 152 | this.nock 153 | .post(`/job/${this.jobName}/build`) 154 | .reply(302, "", { location: "http://localhost:8080/queue/item/5/" }); 155 | 156 | const number = await this.jenkins.job.build(this.jobName); 157 | should(number).be.type("number"); 158 | should(number).be.above(0); 159 | }); 160 | 161 | it("should start build with token", async function () { 162 | this.nock 163 | .post(`/job/${this.jobName}/build?token=secret`) 164 | .reply(201, "", { location: "http://localhost:8080/queue/item/5/" }); 165 | 166 | const number = await this.jenkins.job.build(this.jobName, { 167 | token: "secret", 168 | }); 169 | should(number).be.type("number"); 170 | should(number).be.above(0); 171 | }); 172 | 173 | nit("should work with parameters", async function () { 174 | this.nock 175 | .post("/job/test/buildWithParameters", { hello: "world" }) 176 | .reply(201); 177 | 178 | const opts = { parameters: { hello: "world" } }; 179 | await this.jenkins.job.build("test", opts); 180 | }); 181 | 182 | nit("should work with form data parameters", async function () { 183 | this.jenkins = new Jenkins({ 184 | baseUrl: this.url, 185 | crumbIssuer: helper.config.crumbIssuer, 186 | formData: require("form-data"), 187 | }); 188 | this.jenkins.on("log", debug("jenkins:client")); 189 | 190 | this.nock 191 | .post("/job/test/buildWithParameters", /filename="oneName"/gm) 192 | .reply(201); 193 | 194 | const opts = { 195 | parameters: { 196 | oneName: Buffer.from("oneValue"), 197 | twoName: "twoValue", 198 | }, 199 | }; 200 | await this.jenkins.job.build("test", opts); 201 | }); 202 | 203 | nit("should work with a token and parameters", async function () { 204 | this.nock 205 | .post("/job/test/buildWithParameters?token=secret", { 206 | hello: "world", 207 | }) 208 | .reply(201); 209 | 210 | const opts = { 211 | parameters: { hello: "world" }, 212 | token: "secret", 213 | }; 214 | await this.jenkins.job.build("test", opts); 215 | }); 216 | }); 217 | 218 | describe("config", function () { 219 | it("should get job config", async function () { 220 | this.nock 221 | .get(`/job/${this.jobName}/config.xml`) 222 | .reply(200, fixtures.jobCreate); 223 | 224 | const config = await this.jenkins.job.config(this.jobName); 225 | should(config).be.type("string"); 226 | should(config).containEql(""); 227 | }); 228 | 229 | it("should update config", async function () { 230 | this.nock 231 | .get(`/job/${this.jobName}/config.xml`) 232 | .reply(200, fixtures.jobCreate) 233 | .post(`/job/${this.jobName}/config.xml`) 234 | .reply(200) 235 | .get(`/job/${this.jobName}/config.xml`) 236 | .reply(200, fixtures.jobUpdate); 237 | 238 | const before = await this.jenkins.job.config(this.jobName); 239 | 240 | const config = before.replace( 241 | "before", 242 | "after" 243 | ); 244 | await this.jenkins.job.config(this.jobName, config); 245 | 246 | const after = await this.jenkins.job.config(this.jobName); 247 | 248 | should(before).not.eql(after); 249 | should(after).containEql("after"); 250 | }); 251 | }); 252 | 253 | describe("copy", function () { 254 | it("should copy job", async function () { 255 | const name = this.jobName + "-new"; 256 | 257 | this.nock 258 | .head(`/job/${name}/api/json`) 259 | .reply(404) 260 | .post(`/createItem?name=${name}&from=${this.jobName}&mode=copy`) 261 | .reply(302) 262 | .head(`/job/${name}/api/json`) 263 | .reply(200); 264 | 265 | const jobs = {}; 266 | 267 | const before = await this.jenkins.job.exists(name); 268 | should(before).equal(false); 269 | 270 | await this.jenkins.job.copy(this.jobName, name); 271 | 272 | const after = await this.jenkins.job.exists(name); 273 | should(after).equal(true); 274 | }); 275 | }); 276 | 277 | describe("create", function () { 278 | it("should create job", async function () { 279 | const name = this.jobName + "-new"; 280 | 281 | this.nock 282 | .head(`/job/${name}/api/json`) 283 | .reply(404) 284 | .post(`/createItem?name=${name}`, fixtures.jobCreate) 285 | .reply(200) 286 | .head(`/job/${name}/api/json`) 287 | .reply(200); 288 | 289 | const before = await this.jenkins.job.exists(name); 290 | should(before).equal(false); 291 | 292 | await this.jenkins.job.create(name, fixtures.jobCreate); 293 | 294 | const after = await this.jenkins.job.exists(name); 295 | should(after).equal(true); 296 | }); 297 | 298 | nit("should return an error if it already exists", async function () { 299 | const error = 300 | 'a job already exists with the name "nodejs-jenkins-test"'; 301 | 302 | this.nock 303 | .post("/createItem?name=test", fixtures.jobCreate) 304 | .reply(400, "", { "x-error": error }); 305 | 306 | await shouldThrow(async () => { 307 | await this.jenkins.job.create("test", fixtures.jobCreate); 308 | }, 'jenkins: job.create: a job already exists with the name "nodejs-jenkins-test"'); 309 | }); 310 | }); 311 | 312 | describe("destroy", function () { 313 | it("should delete job", async function () { 314 | this.nock 315 | .head(`/job/${this.jobName}/api/json`) 316 | .reply(200) 317 | .post(`/job/${this.jobName}/doDelete`) 318 | .reply(302) 319 | .head(`/job/${this.jobName}/api/json`) 320 | .reply(404); 321 | 322 | const before = await this.jenkins.job.exists(this.jobName); 323 | should(before).equal(true); 324 | 325 | await this.jenkins.job.destroy(this.jobName); 326 | 327 | const after = await this.jenkins.job.exists(this.jobName); 328 | should(after).equal(false); 329 | }); 330 | 331 | nit("should return error on failure", async function () { 332 | this.nock.post("/job/test/doDelete").reply(200); 333 | 334 | await shouldThrow(async () => { 335 | await this.jenkins.job.destroy("test"); 336 | }, "jenkins: job.destroy: failed to delete: test"); 337 | }); 338 | }); 339 | 340 | describe("disable", function () { 341 | it("should disable job", async function () { 342 | this.nock 343 | .get(`/job/${this.jobName}/api/json`) 344 | .reply(200, fixtures.jobGet) 345 | .post(`/job/${this.jobName}/disable`) 346 | .reply(302) 347 | .get(`/job/${this.jobName}/api/json`) 348 | .reply(200, fixtures.jobGetDisabled); 349 | 350 | const before = await this.jenkins.job.get(this.jobName); 351 | should(before?.buildable).equal(true); 352 | 353 | await this.jenkins.job.disable(this.jobName); 354 | 355 | const after = await this.jenkins.job.get(this.jobName); 356 | should(after?.buildable).equal(false); 357 | }); 358 | }); 359 | 360 | describe("enable", function () { 361 | it("should enable job", async function () { 362 | this.nock 363 | .post(`/job/${this.jobName}/disable`) 364 | .reply(302) 365 | .get(`/job/${this.jobName}/api/json`) 366 | .reply(200, fixtures.jobGetDisabled) 367 | .post(`/job/${this.jobName}/enable`) 368 | .reply(302) 369 | .get(`/job/${this.jobName}/api/json`) 370 | .reply(200, fixtures.jobGet); 371 | 372 | await this.jenkins.job.disable(this.jobName); 373 | 374 | const before = await this.jenkins.job.get(this.jobName); 375 | should(before?.buildable).equal(false); 376 | 377 | await this.jenkins.job.enable(this.jobName); 378 | 379 | const after = await this.jenkins.job.get(this.jobName); 380 | should(after?.buildable).equal(true); 381 | }); 382 | }); 383 | 384 | describe("exists", function () { 385 | it("should not find job", async function () { 386 | const name = this.jobName + "-nope"; 387 | 388 | this.nock.head(`/job/${name}/api/json`).reply(404); 389 | 390 | const exists = await this.jenkins.job.exists(name); 391 | should(exists).equal(false); 392 | }); 393 | 394 | it("should find job", async function () { 395 | this.nock.head(`/job/${this.jobName}/api/json`).reply(200); 396 | 397 | const exists = await this.jenkins.job.exists(this.jobName); 398 | should(exists).equal(true); 399 | }); 400 | }); 401 | 402 | describe("get", function () { 403 | it("should not get job", async function () { 404 | const name = this.jobName + "-nope"; 405 | 406 | this.nock.get(`/job/${name}/api/json`).reply(404); 407 | 408 | await shouldThrow(async () => { 409 | await this.jenkins.job.get(name); 410 | }, `jenkins: job.get: ${name} not found`); 411 | }); 412 | 413 | it("should get job", async function () { 414 | this.nock 415 | .get(`/job/${this.jobName}/api/json`) 416 | .reply(200, fixtures.jobGet); 417 | 418 | const data = await this.jenkins.job.get(this.jobName); 419 | should(data).properties("name", "url"); 420 | }); 421 | 422 | nit("should work with options", async function () { 423 | this.nock 424 | .get("/job/test/api/json?depth=1") 425 | .reply(200, fixtures.jobCreate); 426 | 427 | await this.jenkins.job.get("test", { depth: 1 }); 428 | }); 429 | 430 | nit("should return error when not found", async function () { 431 | this.nock.get("/job/test/api/json").reply(404); 432 | 433 | await shouldThrow(async () => { 434 | await this.jenkins.job.get("test"); 435 | }, "jenkins: job.get: test not found"); 436 | }); 437 | }); 438 | 439 | describe("list", function () { 440 | it("should list jobs", async function () { 441 | this.nock.get("/api/json").reply(200, fixtures.jobList); 442 | 443 | const data = await this.jenkins.job.list(); 444 | should(data).not.be.empty(); 445 | for (const job of data) { 446 | should(job).have.properties("name"); 447 | } 448 | }); 449 | 450 | it("should list jobs with string options", async function () { 451 | this.nock.get("/job/test/api/json").reply(200, fixtures.jobList); 452 | 453 | const jobs = await this.jenkins.job.list("test"); 454 | for (const job of jobs) { 455 | should(job).have.properties("name"); 456 | } 457 | }); 458 | 459 | it("should list jobs with object options", async function () { 460 | this.nock.get("/job/test/api/json").reply(200, fixtures.jobList); 461 | 462 | const jobs = await this.jenkins.job.list({ name: ["test"] }); 463 | for (const job of jobs) { 464 | should(job).have.properties("name"); 465 | } 466 | }); 467 | 468 | nit("should handle corrupt responses", async function () { 469 | const data = '"trash'; 470 | 471 | this.nock.get("/api/json").reply(200, data); 472 | 473 | await shouldThrow(async () => { 474 | await this.jenkins.job.list(); 475 | }, "jenkins: job.list: returned bad data"); 476 | }); 477 | }); 478 | }); 479 | 480 | describe("credentials", function () { 481 | before(async function () { 482 | return helper.setup({ job: false, test: this, folder: true }); 483 | }); 484 | 485 | describe("folder", function () { 486 | it("should create credentials", async function () { 487 | const folder = this.folderName; 488 | const store = "folder"; 489 | const domain = "_"; 490 | const id = "user-new-cred"; 491 | 492 | this.nock 493 | .head( 494 | `/job/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/api/json` 495 | ) 496 | .reply(404) 497 | .post( 498 | `/job/${folder}/credentials/store/${store}/domain/${domain}/createCredentials`, 499 | fixtures.credentialCreate 500 | ) 501 | .reply(200) 502 | .head( 503 | `/job/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/api/json` 504 | ) 505 | .reply(200); 506 | 507 | const before = await this.jenkins.credentials.exists( 508 | id, 509 | folder, 510 | store, 511 | domain 512 | ); 513 | should(before).equal(false); 514 | 515 | await this.jenkins.credentials.create( 516 | folder, 517 | store, 518 | domain, 519 | fixtures.credentialCreate 520 | ); 521 | 522 | const after = await this.jenkins.credentials.exists( 523 | id, 524 | folder, 525 | store, 526 | domain 527 | ); 528 | should(after).equal(true); 529 | }); 530 | 531 | it("should get credentials", async function () { 532 | const folder = this.folderName; 533 | const store = "folder"; 534 | const domain = "_"; 535 | const id = "user-new-cred"; 536 | 537 | this.nock 538 | .get( 539 | `/job/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/config.xml` 540 | ) 541 | .reply(200, fixtures.credentialCreate); 542 | 543 | const config = await this.jenkins.credentials.config( 544 | id, 545 | folder, 546 | store, 547 | domain 548 | ); 549 | should(config).be.type("string"); 550 | should(config).equals(fixtures.credentialCreate); 551 | }); 552 | 553 | it("should update credentials", async function () { 554 | const folder = this.folderName; 555 | const store = "folder"; 556 | const domain = "_"; 557 | const id = "user-new-cred"; 558 | 559 | this.nock 560 | .get( 561 | `/job/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/config.xml` 562 | ) 563 | .reply(200, fixtures.credentialCreate) 564 | .post( 565 | `/job/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/config.xml` 566 | ) 567 | .reply(200) 568 | .get( 569 | `/job/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/config.xml` 570 | ) 571 | .reply(200, fixtures.credentialUpdate); 572 | 573 | const before = await this.jenkins.credentials.config( 574 | id, 575 | folder, 576 | store, 577 | domain 578 | ); 579 | 580 | const config = before.replace( 581 | "admin", 582 | "updated" 583 | ); 584 | 585 | await this.jenkins.credentials.config( 586 | id, 587 | folder, 588 | store, 589 | domain, 590 | config 591 | ); 592 | 593 | const after = await this.jenkins.credentials.config( 594 | id, 595 | folder, 596 | store, 597 | domain 598 | ); 599 | should(config).be.type("string"); 600 | should(config).equals(fixtures.credentialUpdate); 601 | }); 602 | 603 | it("should list credentials", async function () { 604 | const folder = this.folderName; 605 | const store = "folder"; 606 | const domain = "_"; 607 | 608 | this.nock 609 | .get( 610 | `/job/${folder}/credentials/store/${store}/domain/${domain}/api/json?tree=credentials[id]` 611 | ) 612 | .reply(200, fixtures.credentialList); 613 | 614 | const data = await this.jenkins.credentials.list(folder, store, domain); 615 | should(data).not.be.empty(); 616 | for (const credential of data) { 617 | should(credential).have.properties("id"); 618 | } 619 | }); 620 | 621 | it("should delete credential", async function () { 622 | const folder = this.folderName; 623 | const store = "folder"; 624 | const domain = "_"; 625 | const id = "user-new-cred"; 626 | 627 | this.nock 628 | .head( 629 | `/job/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/api/json` 630 | ) 631 | .reply(200) 632 | .delete( 633 | `/job/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/config.xml` 634 | ) 635 | .reply(200) 636 | .head( 637 | `/job/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/api/json` 638 | ) 639 | .reply(404); 640 | 641 | const jobs = {}; 642 | 643 | const before = await this.jenkins.credentials.exists( 644 | id, 645 | folder, 646 | store, 647 | domain 648 | ); 649 | should(before).equal(true); 650 | 651 | await this.jenkins.credentials.destroy(id, folder, store, domain); 652 | 653 | const after = await this.jenkins.credentials.exists( 654 | id, 655 | folder, 656 | store, 657 | domain 658 | ); 659 | should(after).equal(false); 660 | }); 661 | }); 662 | 663 | describe("system", function () { 664 | it("should create system credentials", async function () { 665 | const folder = "manage"; 666 | const store = "system"; 667 | const domain = "_"; 668 | const id = "user-new-cred"; 669 | 670 | this.nock 671 | .head( 672 | `/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/api/json` 673 | ) 674 | .reply(404) 675 | .post( 676 | `/${folder}/credentials/store/${store}/domain/${domain}/createCredentials`, 677 | fixtures.credentialCreate 678 | ) 679 | .reply(200) 680 | .head( 681 | `/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/api/json` 682 | ) 683 | .reply(200); 684 | 685 | const before = await this.jenkins.credentials.exists( 686 | id, 687 | folder, 688 | store, 689 | domain 690 | ); 691 | should(before).equal(false); 692 | 693 | await this.jenkins.credentials.create( 694 | folder, 695 | store, 696 | domain, 697 | fixtures.credentialCreate 698 | ); 699 | 700 | const after = await this.jenkins.credentials.exists( 701 | id, 702 | folder, 703 | store, 704 | domain 705 | ); 706 | should(after).equal(true); 707 | }); 708 | 709 | it("should get system credentials", async function () { 710 | const folder = "manage"; 711 | const store = "system"; 712 | const domain = "_"; 713 | const id = "user-new-cred"; 714 | 715 | this.nock 716 | .get( 717 | `/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/config.xml` 718 | ) 719 | .reply(200, fixtures.credentialCreate); 720 | 721 | const config = await this.jenkins.credentials.config( 722 | id, 723 | folder, 724 | store, 725 | domain 726 | ); 727 | should(config).be.type("string"); 728 | should(config).equals(fixtures.credentialCreate); 729 | }); 730 | 731 | it("should update system credentials", async function () { 732 | const folder = "manage"; 733 | const store = "system"; 734 | const domain = "_"; 735 | const id = "user-new-cred"; 736 | 737 | this.nock 738 | .get( 739 | `/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/config.xml` 740 | ) 741 | .reply(200, fixtures.credentialCreate) 742 | .post( 743 | `/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/config.xml` 744 | ) 745 | .reply(200) 746 | .get( 747 | `/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/config.xml` 748 | ) 749 | .reply(200, fixtures.credentialUpdate); 750 | 751 | const before = await this.jenkins.credentials.config( 752 | id, 753 | folder, 754 | store, 755 | domain 756 | ); 757 | 758 | const config = before.replace( 759 | "admin", 760 | "updated" 761 | ); 762 | 763 | await this.jenkins.credentials.config( 764 | id, 765 | folder, 766 | store, 767 | domain, 768 | config 769 | ); 770 | 771 | const after = await this.jenkins.credentials.config( 772 | id, 773 | folder, 774 | store, 775 | domain 776 | ); 777 | should(config).be.type("string"); 778 | should(config).equals(fixtures.credentialUpdate); 779 | }); 780 | 781 | it("should list system credentials", async function () { 782 | const folder = "manage"; 783 | const store = "system"; 784 | const domain = "_"; 785 | 786 | this.nock 787 | .get( 788 | `/${folder}/credentials/store/${store}/domain/${domain}/api/json?tree=credentials[id]` 789 | ) 790 | .reply(200, fixtures.credentialList); 791 | 792 | const data = await this.jenkins.credentials.list(folder, store, domain); 793 | should(data).not.be.empty(); 794 | for (const credential of data) { 795 | should(credential).have.properties("id"); 796 | } 797 | }); 798 | 799 | it("should delete system credential", async function () { 800 | const folder = "manage"; 801 | const store = "system"; 802 | const domain = "_"; 803 | const id = "user-new-cred"; 804 | 805 | this.nock 806 | .head( 807 | `/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/api/json` 808 | ) 809 | .reply(200) 810 | .delete( 811 | `/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/config.xml` 812 | ) 813 | .reply(200) 814 | .head( 815 | `/${folder}/credentials/store/${store}/domain/${domain}/credential/${id}/api/json` 816 | ) 817 | .reply(404); 818 | 819 | const jobs = {}; 820 | 821 | const before = await this.jenkins.credentials.exists( 822 | id, 823 | folder, 824 | store, 825 | domain 826 | ); 827 | should(before).equal(true); 828 | 829 | await this.jenkins.credentials.destroy(id, folder, store, domain); 830 | 831 | const after = await this.jenkins.credentials.exists( 832 | id, 833 | folder, 834 | store, 835 | domain 836 | ); 837 | should(after).equal(false); 838 | }); 839 | }); 840 | }); 841 | 842 | describe("label", function () { 843 | beforeEach(async function () { 844 | return helper.setup({ test: this }); 845 | }); 846 | 847 | describe("get", function () { 848 | it("should get label details", async function () { 849 | this.nock.get("/label/master/api/json").reply(200, fixtures.labelGet); 850 | 851 | const label = await this.jenkins.label.get("master"); 852 | should(label).have.properties("nodes"); 853 | }); 854 | }); 855 | }); 856 | 857 | describe("node", function () { 858 | beforeEach(async function () { 859 | return helper.setup({ node: true, test: this }); 860 | }); 861 | 862 | describe("config", function () { 863 | it("should error on master update", async function () { 864 | await shouldThrow(async () => { 865 | await this.jenkins.node.config("master", "xml"); 866 | }, "jenkins: node.config: master not supported"); 867 | }); 868 | }); 869 | 870 | describe("create", function () { 871 | it("should create node", async function () { 872 | const name = `test-node-${uuid.v4()}`; 873 | 874 | this.nock 875 | .post( 876 | "/computer/doCreateItem?" + 877 | fixtures.nodeCreateQuery.replace(/{name}/g, name) 878 | ) 879 | .reply(302, "", { location: "http://localhost:8080/computer/" }); 880 | 881 | await this.jenkins.node.create(name); 882 | }); 883 | }); 884 | 885 | describe("destroy", function () { 886 | it("should delete node", async function () { 887 | this.nock 888 | .head(`/computer/${this.nodeName}/api/json`) 889 | .reply(200) 890 | .post(`/computer/${this.nodeName}/doDelete`) 891 | .reply(302, "") 892 | .head(`/computer/${this.nodeName}/api/json`) 893 | .reply(404); 894 | 895 | const jobs = {}; 896 | 897 | const before = await this.jenkins.node.exists(this.nodeName); 898 | should(before).equal(true); 899 | 900 | await this.jenkins.node.destroy(this.nodeName); 901 | 902 | const after = await this.jenkins.node.exists(this.nodeName); 903 | should(after).equal(false); 904 | }); 905 | }); 906 | 907 | describe("disable", function () { 908 | it("should disable node", async function () { 909 | this.nock 910 | .get(`/computer/${this.nodeName}/api/json`) 911 | .reply(200, fixtures.nodeGet) 912 | .get(`/computer/${this.nodeName}/api/json`) 913 | .reply(200, fixtures.nodeGet) 914 | .post(`/computer/${this.nodeName}/toggleOffline?offlineMessage=away`) 915 | .reply(302, "") 916 | .get(`/computer/${this.nodeName}/api/json`) 917 | .reply(200, fixtures.nodeGetTempOffline) 918 | .get(`/computer/${this.nodeName}/api/json`) 919 | .reply(200, fixtures.nodeGetTempOffline) 920 | .post( 921 | `/computer/${this.nodeName}/changeOfflineCause`, 922 | "offlineMessage=update&json=%7B%22offlineMessage%22%3A%22update%22%7D&" + 923 | "Submit=Update%20reason" 924 | ) 925 | .reply(302, "") 926 | .get(`/computer/${this.nodeName}/api/json`) 927 | .reply(200, fixtures.nodeGetTempOfflineUpdate); 928 | 929 | const beforeDisable = await this.jenkins.node.get(this.nodeName); 930 | should(beforeDisable?.temporarilyOffline).equal(false); 931 | 932 | await this.jenkins.node.disable(this.nodeName, "away"); 933 | 934 | const afterDisable = await this.jenkins.node.get(this.nodeName); 935 | should(afterDisable?.temporarilyOffline).equal(true); 936 | should(afterDisable?.offlineCauseReason).equal("away"); 937 | 938 | await this.jenkins.node.disable(this.nodeName, "update"); 939 | 940 | const afterUpdate = await this.jenkins.node.get(this.nodeName); 941 | should(afterUpdate?.temporarilyOffline).equal(true); 942 | should(afterUpdate?.offlineCauseReason).equal("update"); 943 | }); 944 | }); 945 | 946 | describe("enable", function () { 947 | it("should enable node", async function () { 948 | this.nock 949 | .get(`/computer/${this.nodeName}/api/json`) 950 | .reply(200, fixtures.nodeGet) 951 | .post(`/computer/${this.nodeName}/toggleOffline?offlineMessage=away`) 952 | .reply(302, "") 953 | .get(`/computer/${this.nodeName}/api/json`) 954 | .reply(200, fixtures.nodeGetTempOffline) 955 | .get(`/computer/${this.nodeName}/api/json`) 956 | .reply(200, fixtures.nodeGetTempOffline) 957 | .get(`/computer/${this.nodeName}/api/json`) 958 | .reply(200, fixtures.nodeGet) 959 | .post(`/computer/${this.nodeName}/toggleOffline?offlineMessage=`) 960 | .reply(302, ""); 961 | 962 | await this.jenkins.node.disable(this.nodeName, "away"); 963 | 964 | const before = await this.jenkins.node.get(this.nodeName); 965 | should(before?.temporarilyOffline).equal(true); 966 | 967 | await this.jenkins.node.enable(this.nodeName); 968 | 969 | const after = await this.jenkins.node.get(this.nodeName); 970 | should(after?.temporarilyOffline).equal(false); 971 | }); 972 | }); 973 | 974 | describe("disconnect", function () { 975 | it("should disconnect node", async function () { 976 | this.nock 977 | .get(`/computer/${this.nodeName}/api/json`) 978 | .reply(200, fixtures.nodeGetOnline) 979 | .get(`/computer/${this.nodeName}/api/json`) 980 | .reply(200, fixtures.nodeGetOnline) 981 | .post(`/computer/${this.nodeName}/doDisconnect?offlineMessage=away`) 982 | .reply(302, "") 983 | .get(`/computer/${this.nodeName}/api/json`) 984 | .reply(200, fixtures.nodeGetOffline) 985 | .get(`/computer/${this.nodeName}/api/json`) 986 | .reply(200, fixtures.nodeGetOffline) 987 | .post( 988 | `/computer/${this.nodeName}/toggleOffline?offlineMessage=update` 989 | ) 990 | .reply(302, "") 991 | .get(`/computer/${this.nodeName}/api/json`) 992 | .reply(200, fixtures.nodeGetOfflineUpdate); 993 | 994 | const beforeDisconnect = await retry(1000, async () => { 995 | const node = await this.jenkins.node.get(this.nodeName); 996 | if (!node || node.offline) throw new Error("node offline"); 997 | return node; 998 | }); 999 | should(beforeDisconnect?.offline).equal(false); 1000 | 1001 | await this.jenkins.node.disconnect(this.nodeName, "away"); 1002 | 1003 | const afterDisconnect = await this.jenkins.node.get(this.nodeName); 1004 | should(afterDisconnect?.offline).equal(true); 1005 | should(afterDisconnect?.offlineCauseReason).equal("away"); 1006 | 1007 | await this.jenkins.node.disconnect(this.nodeName, "update"); 1008 | 1009 | const afterUpdate = await this.jenkins.node.get(this.nodeName); 1010 | should(afterUpdate?.offline).equal(true); 1011 | should(afterUpdate?.offlineCauseReason).equal("update"); 1012 | }); 1013 | }); 1014 | 1015 | describe("exists", function () { 1016 | it("should not find node", async function () { 1017 | const name = this.nodeName + "-nope"; 1018 | 1019 | this.nock.head(`/computer/${name}/api/json`).reply(404); 1020 | 1021 | const exists = await this.jenkins.node.exists(name); 1022 | should(exists).equal(false); 1023 | }); 1024 | 1025 | it("should find node", async function () { 1026 | this.nock.head(`/computer/${this.nodeName}/api/json`).reply(200); 1027 | 1028 | const exists = await this.jenkins.node.exists(this.nodeName); 1029 | should(exists).equal(true); 1030 | }); 1031 | }); 1032 | 1033 | describe("get", function () { 1034 | it("should get node details", async function () { 1035 | this.nock 1036 | .get(`/computer/${this.nodeName}/api/json`) 1037 | .reply(200, fixtures.nodeGet); 1038 | 1039 | const node = await this.jenkins.node.get(this.nodeName); 1040 | should(node).have.properties("displayName"); 1041 | }); 1042 | 1043 | it("should get master", async function () { 1044 | this.nock 1045 | .get("/computer/(master)/api/json") 1046 | .reply(200, fixtures.nodeGet); 1047 | 1048 | const node = await this.jenkins.node.get("master"); 1049 | should(node).have.properties("displayName"); 1050 | }); 1051 | }); 1052 | 1053 | describe("list", function () { 1054 | it("should list nodes", async function () { 1055 | this.nock.get("/computer/api/json").reply(200, fixtures.nodeList); 1056 | 1057 | const nodes = await this.jenkins.node.list(); 1058 | should.exist(nodes); 1059 | should(nodes).be.instanceof(Array); 1060 | should(nodes).not.be.empty; 1061 | }); 1062 | 1063 | it("should include extra metadata", async function () { 1064 | this.nock.get("/computer/api/json").reply(200, fixtures.nodeList); 1065 | 1066 | const info = await this.jenkins.node.list({ full: true }); 1067 | should(info).have.properties( 1068 | "busyExecutors", 1069 | "computer", 1070 | "displayName", 1071 | "totalExecutors" 1072 | ); 1073 | }); 1074 | }); 1075 | }); 1076 | 1077 | describe("queue", function () { 1078 | beforeEach(async function () { 1079 | return helper.setup({ job: true, test: this }); 1080 | }); 1081 | 1082 | describe("list", function () { 1083 | it("should list queue", async function () { 1084 | this.nock 1085 | .get("/queue/api/json") 1086 | .reply(200, fixtures.queueList) 1087 | .post(`/job/${this.jobName}/build`) 1088 | .reply(201, "", { 1089 | location: "http://localhost:8080/queue/item/124/", 1090 | }); 1091 | 1092 | let stop = false; 1093 | 1094 | await Promise.all([ 1095 | retry(1000, async () => { 1096 | const queue = await this.jenkins.queue.list(); 1097 | if (!queue?.length) { 1098 | throw new Error("no queue"); 1099 | } 1100 | 1101 | stop = true; 1102 | 1103 | should(queue).be.instanceof(Array); 1104 | }), 1105 | retry(1000, async () => { 1106 | if (stop) return; 1107 | 1108 | await this.jenkins.job.build(this.jobName); 1109 | 1110 | if (!stop) throw new Error("queue more"); 1111 | }), 1112 | ]); 1113 | }); 1114 | }); 1115 | 1116 | describe("item", function () { 1117 | nit("should return a queue item", async function () { 1118 | this.nock 1119 | .get("/queue/item/130/api/json") 1120 | .reply(200, fixtures.queueItem); 1121 | 1122 | const data = await this.jenkins.queue.item(130); 1123 | should(data).have.property("id"); 1124 | should(data.id).equal(130); 1125 | }); 1126 | 1127 | it("should require a number", async function () { 1128 | await shouldThrow(async () => { 1129 | await this.jenkins.queue.item(null); 1130 | }, "jenkins: queue.item: number required"); 1131 | }); 1132 | }); 1133 | 1134 | describe("get", function () { 1135 | nit("should work", async function () { 1136 | this.nock 1137 | .get("/computer/(master)/api/json") 1138 | .reply(200, fixtures.nodeGet); 1139 | 1140 | const data = await this.jenkins.node.get("master"); 1141 | should.exist(data); 1142 | }); 1143 | }); 1144 | 1145 | ndescribe("cancel", function () { 1146 | it("should work", async function () { 1147 | this.nock.post("/queue/item/1/cancelQueue", "").reply(302); 1148 | 1149 | await this.jenkins.queue.cancel(1); 1150 | }); 1151 | 1152 | it("should return error on failure", async function () { 1153 | this.nock.post("/queue/item/1/cancelQueue", "").reply(500); 1154 | 1155 | await shouldThrow(async () => { 1156 | await this.jenkins.queue.cancel(1); 1157 | }, "jenkins: queue.cancel: failed to cancel: 1"); 1158 | }); 1159 | }); 1160 | }); 1161 | 1162 | describe("view", function () { 1163 | beforeEach(async function () { 1164 | return helper.setup({ job: true, view: true, test: this }); 1165 | }); 1166 | 1167 | describe("create", function () { 1168 | it("should create view", async function () { 1169 | const name = this.viewName + "-new"; 1170 | 1171 | this.nock 1172 | .head(`/view/${name}/api/json`) 1173 | .reply(404) 1174 | .post( 1175 | "/createView", 1176 | JSON.parse( 1177 | JSON.stringify(fixtures.viewCreate).replace(/test-view/g, name) 1178 | ) 1179 | ) 1180 | .reply(302) 1181 | .head(`/view/${name}/api/json`) 1182 | .reply(200); 1183 | 1184 | const before = await this.jenkins.view.exists(name); 1185 | should(before).equal(false); 1186 | 1187 | await this.jenkins.view.create(name, "list"); 1188 | 1189 | const after = await this.jenkins.view.exists(name); 1190 | should(after).equal(true); 1191 | }); 1192 | 1193 | nit("should return an error if it already exists", async function () { 1194 | const error = 'A view already exists with the name "test-view"'; 1195 | 1196 | this.nock 1197 | .post("/createView", fixtures.viewCreate) 1198 | .reply(400, "", { "x-error": error }); 1199 | 1200 | await shouldThrow(async () => { 1201 | await this.jenkins.view.create(this.viewName, "list"); 1202 | }, 'jenkins: view.create: A view already exists with the name "test-view"'); 1203 | }); 1204 | }); 1205 | 1206 | describe("config", function () { 1207 | it("should return xml", async function () { 1208 | this.nock 1209 | .get(`/view/${this.viewName}/config.xml`) 1210 | .reply(200, fixtures.viewConfig); 1211 | 1212 | const config = await this.jenkins.view.config(this.viewName); 1213 | should(config).be.type("string"); 1214 | should(config).containEql(""); 1215 | }); 1216 | 1217 | it("should update config xml", async function () { 1218 | const src = "false"; 1219 | const dst = "true"; 1220 | 1221 | this.nock 1222 | .get(`/view/${this.viewName}/config.xml`) 1223 | .reply(200, fixtures.viewConfig) 1224 | .post(`/view/${this.viewName}/config.xml`) 1225 | .reply(200) 1226 | .get(`/view/${this.viewName}/config.xml`) 1227 | .reply(200, fixtures.viewConfig.replace(src, dst)); 1228 | 1229 | const before = await this.jenkins.view.config(this.viewName); 1230 | should(before).containEql(src); 1231 | 1232 | const config = fixtures.viewConfig.replace(src, dst); 1233 | await this.jenkins.view.config(this.viewName, config); 1234 | 1235 | const after = await this.jenkins.view.config(this.viewName); 1236 | should(after).containEql(dst); 1237 | }); 1238 | }); 1239 | 1240 | describe("destroy", function () { 1241 | it("should delete view", async function () { 1242 | this.nock 1243 | .head(`/view/${this.viewName}/api/json`) 1244 | .reply(200) 1245 | .post(`/view/${this.viewName}/doDelete`) 1246 | .reply(302) 1247 | .head(`/view/${this.viewName}/api/json`) 1248 | .reply(404); 1249 | 1250 | const before = await this.jenkins.view.exists(this.viewName); 1251 | should(before).equal(true); 1252 | 1253 | await this.jenkins.view.destroy(this.viewName); 1254 | 1255 | const after = await this.jenkins.view.exists(this.viewName); 1256 | should(after).equal(false); 1257 | }); 1258 | 1259 | nit("should return error on failure", async function () { 1260 | this.nock.post("/view/test/doDelete").reply(200); 1261 | 1262 | await shouldThrow(async () => { 1263 | await this.jenkins.view.destroy("test"); 1264 | }, "jenkins: view.destroy: failed to delete: test"); 1265 | }); 1266 | }); 1267 | 1268 | describe("get", function () { 1269 | it("should not get view", async function () { 1270 | const name = this.viewName + "-nope"; 1271 | 1272 | this.nock.get(`/view/${name}/api/json`).reply(404); 1273 | 1274 | await shouldThrow(async () => { 1275 | await this.jenkins.view.get(name); 1276 | }, `jenkins: view.get: ${name} not found`); 1277 | }); 1278 | 1279 | it("should get view", async function () { 1280 | this.nock 1281 | .get(`/view/${this.viewName}/api/json`) 1282 | .reply(200, fixtures.viewGet); 1283 | 1284 | const data = await this.jenkins.view.get(this.viewName); 1285 | should(data).properties("name", "url"); 1286 | }); 1287 | 1288 | nit("should work with options", async function () { 1289 | this.nock 1290 | .get("/view/test/api/json?depth=1") 1291 | .reply(200, fixtures.viewCreate); 1292 | 1293 | await this.jenkins.view.get("test", { depth: 1 }); 1294 | }); 1295 | 1296 | nit("should return error when not found", async function () { 1297 | this.nock.get("/view/test/api/json").reply(404); 1298 | 1299 | await shouldThrow(async () => { 1300 | await this.jenkins.view.get("test"); 1301 | }, "jenkins: view.get: test not found"); 1302 | }); 1303 | }); 1304 | 1305 | describe("list", function () { 1306 | it("should list views", async function () { 1307 | this.nock.get("/api/json").reply(200, fixtures.viewList); 1308 | 1309 | const data = await this.jenkins.view.list(); 1310 | should(data).not.be.empty(); 1311 | for (const view of data) { 1312 | should(view).have.properties("name"); 1313 | } 1314 | }); 1315 | 1316 | nit("should handle corrupt responses", async function () { 1317 | const data = '"trash'; 1318 | 1319 | this.nock.get("/api/json").reply(200, data); 1320 | 1321 | await shouldThrow(async () => { 1322 | await this.jenkins.view.list(); 1323 | }, "jenkins: view.list: returned bad data"); 1324 | }); 1325 | }); 1326 | 1327 | describe("add", function () { 1328 | it("should add job to view", async function () { 1329 | const beforeView = fixtures.viewGetListView; 1330 | beforeView.jobs = []; 1331 | 1332 | this.nock 1333 | .get(`/view/${this.viewName}/api/json`) 1334 | .reply(200, beforeView) 1335 | .post(`/view/${this.viewName}/addJobToView?name=` + this.jobName) 1336 | .reply(200) 1337 | .get(`/view/${this.viewName}/api/json`) 1338 | .reply(200, fixtures.viewGetListView); 1339 | 1340 | const before = await this.jenkins.view.get(this.viewName); 1341 | should(before?.jobs).be.empty(); 1342 | 1343 | await this.jenkins.view.add(this.viewName, this.jobName); 1344 | 1345 | const after = await this.jenkins.view.get(this.viewName); 1346 | should(after?.jobs).not.be.empty(); 1347 | }); 1348 | }); 1349 | 1350 | describe("remove", function () { 1351 | it("should remove job from view", async function () { 1352 | const afterView = fixtures.viewGetListView; 1353 | afterView.jobs = []; 1354 | 1355 | this.nock 1356 | .post(`/view/${this.viewName}/addJobToView?name=` + this.jobName) 1357 | .reply(200) 1358 | .get(`/view/${this.viewName}/api/json`) 1359 | .reply(200, fixtures.viewGetListView) 1360 | .post(`/view/${this.viewName}/removeJobFromView?name=` + this.jobName) 1361 | .reply(200) 1362 | .get(`/view/${this.viewName}/api/json`) 1363 | .reply(200, afterView); 1364 | 1365 | await this.jenkins.view.add(this.viewName, this.jobName); 1366 | 1367 | const before = await this.jenkins.view.get(this.viewName); 1368 | should(before?.jobs).not.empty(); 1369 | 1370 | await this.jenkins.view.remove(this.viewName, this.jobName); 1371 | 1372 | const after = await this.jenkins.view.get(this.viewName); 1373 | should(after?.jobs).be.empty(); 1374 | }); 1375 | }); 1376 | }); 1377 | 1378 | describe("plugin", function () { 1379 | beforeEach(async function () { 1380 | return helper.setup({ test: this }); 1381 | }); 1382 | 1383 | describe("list", function () { 1384 | it("should list plugins", async function () { 1385 | this.nock 1386 | .get("/pluginManager/api/json?depth=2") 1387 | .reply(200, fixtures.pluginList); 1388 | 1389 | const plugins = await this.jenkins.plugin.list({ depth: 2 }); 1390 | should(plugins).be.instanceof(Array); 1391 | should(plugins).not.be.empty(); 1392 | 1393 | for (const plugin of plugins) { 1394 | should(plugin).have.properties([ 1395 | "longName", 1396 | "shortName", 1397 | "dependencies", 1398 | ]); 1399 | } 1400 | }); 1401 | }); 1402 | }); 1403 | }); 1404 | 1405 | async function shouldThrow(block, message) { 1406 | if (!message) throw new Error("expected message required"); 1407 | 1408 | try { 1409 | await block(); 1410 | } catch (err) { 1411 | should(err?.message).equal(message); 1412 | return; 1413 | } 1414 | 1415 | throw Error("no exception thrown"); 1416 | } 1417 | -------------------------------------------------------------------------------- /test/scriptApproval.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | b4e7136f5f17cb38885d051a46ac3de5544bcebb 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/setup.groovy: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | import com.cloudbees.hudson.plugins.folder.* 4 | import jenkins.model.* 5 | import hudson.security.* 6 | import jenkins.security.s2m.AdminWhitelistRule 7 | 8 | def instance = Jenkins.getInstance() 9 | 10 | def hudsonRealm = new HudsonPrivateSecurityRealm(false) 11 | hudsonRealm.createAccount("admin", "admin") 12 | instance.setSecurityRealm(hudsonRealm) 13 | 14 | instance.createProject(Folder.class, "test") 15 | 16 | def strategy = new FullControlOnceLoggedInAuthorizationStrategy() 17 | instance.setAuthorizationStrategy(strategy) 18 | instance.save() 19 | 20 | instance.getInjector().getInstance(AdminWhitelistRule.class).setMasterKillSwitch(false) 21 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const should = require("should"); 3 | 4 | const utils = require("../lib/utils"); 5 | 6 | describe("utils", function () { 7 | describe("folderPath", function () { 8 | describe("constructor", function () { 9 | it("should parse string", function () { 10 | should(utils.folderPath().value).eql([]); 11 | should(utils.folderPath("").value).eql([]); 12 | should(utils.folderPath("/").value).eql([]); 13 | should(utils.folderPath("a/").value).eql(["a"]); 14 | should(utils.folderPath("/a").value).eql(["a"]); 15 | should(utils.folderPath("a/b").value).eql(["a", "b"]); 16 | should(utils.folderPath("a//b").value).eql(["a", "b"]); 17 | should(utils.folderPath("a/b/c").value).eql(["a", "b", "c"]); 18 | }); 19 | 20 | it("should parse url", function () { 21 | for (const prefix of ["http://", "https://"]) { 22 | should(utils.folderPath(prefix).value).eql([]); 23 | should(utils.folderPath(`${prefix}example.org/`).value).eql([]); 24 | should(utils.folderPath(`${prefix}example.org/job/one`).value).eql([ 25 | "one", 26 | ]); 27 | should( 28 | utils.folderPath(`${prefix}example.org/proxy/job/one`).value 29 | ).eql(["one"]); 30 | should( 31 | utils.folderPath(`${prefix}example.org/job/one/hello/world`).value 32 | ).eql(["one"]); 33 | should( 34 | utils.folderPath(`${prefix}example.org/job/one/hello/job/nope`) 35 | .value 36 | ).eql(["one"]); 37 | should( 38 | utils.folderPath(`${prefix}example.org/job/one/job/two`).value 39 | ).eql(["one", "two"]); 40 | should( 41 | utils.folderPath(`${prefix}example.org/job/one%2Ftwo`).value 42 | ).eql(["one/two"]); 43 | should( 44 | utils.folderPath(`${prefix}example.org/job/one/job/two%252Fthree/`) 45 | .value 46 | ).eql(["one", "two%2Fthree"]); 47 | } 48 | }); 49 | 50 | it("should parse array", function () { 51 | should(utils.folderPath(["a"]).value).eql(["a"]); 52 | should(utils.folderPath(["a", "b"]).value).eql(["a", "b"]); 53 | }); 54 | }); 55 | 56 | describe("name", function () { 57 | it("should work", function () { 58 | should(utils.folderPath().name()).equal(""); 59 | should(utils.folderPath("a").name()).equal("a"); 60 | should(utils.folderPath("a/b").name()).equal("b"); 61 | }); 62 | }); 63 | 64 | describe("path", function () { 65 | it("should work", function () { 66 | should(utils.folderPath().path()).containEql({ 67 | encode: false, 68 | value: "", 69 | }); 70 | should(utils.folderPath("a").path()).containEql({ 71 | encode: false, 72 | value: "/job/a", 73 | }); 74 | should(utils.folderPath("a/b").path()).containEql({ 75 | encode: false, 76 | value: "/job/a/job/b", 77 | }); 78 | }); 79 | }); 80 | 81 | describe("parent", function () { 82 | it("should work", function () { 83 | should(utils.folderPath().parent().value).eql([]); 84 | should(utils.folderPath("a").parent().value).eql([]); 85 | should(utils.folderPath("a/b").parent().value).eql(["a"]); 86 | should(utils.folderPath("a/b/c").parent().value).eql(["a", "b"]); 87 | }); 88 | }); 89 | }); 90 | 91 | describe("parse", function () { 92 | it("should work", function () { 93 | should(utils.parse([])).eql({}); 94 | should(utils.parse(["value"])).eql({}); 95 | should(utils.parse([{}])).eql({}); 96 | should(utils.parse([{ hello: "world" }])).eql({ hello: "world" }); 97 | should(utils.parse([], "name")).eql({}); 98 | should(utils.parse(["test"], "name")).eql({ name: "test" }); 99 | should(utils.parse(["test", { hello: "world" }], "name")).eql({ 100 | hello: "world", 101 | name: "test", 102 | }); 103 | should(utils.parse(["test", { hello: "world" }], "name", "value")).eql({ 104 | hello: "world", 105 | name: "test", 106 | }); 107 | should( 108 | utils.parse(["one", "two", { hello: "world" }], "name", "value") 109 | ).eql({ hello: "world", name: "one", value: "two" }); 110 | }); 111 | }); 112 | 113 | describe("isFileLike", function () { 114 | it("should work", function () { 115 | should(utils.isFileLike()).is.false; 116 | should(utils.isFileLike(null)).is.false; 117 | should(utils.isFileLike("test")).is.false; 118 | should(utils.isFileLike({})).is.false; 119 | 120 | should(utils.isFileLike(Buffer.from("test"))).is.true; 121 | 122 | const stream = fs.createReadStream(__filename); 123 | should(utils.isFileLike(stream)).is.true; 124 | stream.close(); 125 | }); 126 | }); 127 | }); 128 | --------------------------------------------------------------------------------