├── .nvmrc ├── bin └── jenkins ├── src ├── commands │ ├── run │ │ ├── jobs-builder │ │ │ ├── index.ts │ │ │ ├── models.ts │ │ │ ├── update-job-builder.ts │ │ │ ├── test-job-builder.ts │ │ │ ├── jobs-builder.ts │ │ │ └── deploy-job-builder.ts │ │ ├── questions │ │ │ ├── index.ts │ │ │ ├── param-questions.model.ts │ │ │ ├── job-questions.ts │ │ │ ├── question-helpers.ts │ │ │ ├── project-questions.ts │ │ │ └── param-questions.ts │ │ └── index.ts │ ├── sandbox │ │ └── index.ts │ └── login │ │ └── index.ts ├── utils │ ├── list.ts │ ├── package.ts │ ├── noop.spec.ts │ ├── check-version.ts │ ├── filter-list.ts │ ├── print-header.ts │ ├── sandbox.ts │ ├── store.ts │ └── jenkins.ts └── index.ts ├── .prettierignore ├── .prettierrc ├── .lintstagedrc ├── jest.config.js ├── tsconfig.json ├── .travis.yml ├── tslint.json ├── CONTRIBUTING.md ├── LICENSE ├── .gitignore ├── .npmignore ├── README.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.13.1 2 | -------------------------------------------------------------------------------- /bin/jenkins: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../dist').start(); 3 | -------------------------------------------------------------------------------- /src/commands/run/jobs-builder/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jobs-builder'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/**/* 2 | coverage/**/* 3 | node_modules/**/* 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /src/utils/list.ts: -------------------------------------------------------------------------------- 1 | export function list(val: string): string[] { 2 | return val.split(','); 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/package.ts: -------------------------------------------------------------------------------- 1 | export const packageJson: { [key: string]: any } = require('../../package.json'); 2 | -------------------------------------------------------------------------------- /src/utils/noop.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Index', () => { 2 | it('should export specific number of elements', () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "prettier --write", 4 | "tslint --fix" 5 | ], 6 | "*.{js,json,css,scss,html}": [ 7 | "prettier --write" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/run/questions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './job-questions'; 2 | export * from './param-questions.model'; 3 | export * from './param-questions'; 4 | export * from './project-questions'; 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest', 5 | }, 6 | collectCoverageFrom: ['src/**/*.ts', '!src/**/*.mock.ts'], 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "esnext", 5 | "noImplicitAny": false, 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "sourceMap": false, 9 | "resolveJsonModule": true 10 | }, 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/check-version.ts: -------------------------------------------------------------------------------- 1 | import * as updateNotifier from 'update-notifier'; 2 | import { packageJson } from './package'; 3 | 4 | export function checkVersion() { 5 | const notifier = updateNotifier({ 6 | pkg: packageJson as any, 7 | updateCheckInterval: 1000 * 60 * 60 * 24 * 7, // 1 week 8 | }); 9 | notifier.notify({ isGlobal: true }); 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/run/jobs-builder/models.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JobBatchDescriptor as GenericJobBatchDescriber, 3 | JobDescriptor as GenericJobDescriber, 4 | } from 'jenkins-jobs-runner'; 5 | import { Job } from '../questions/job-questions'; 6 | import { Project } from '../questions/project-questions'; 7 | 8 | export type JobBatchDescriptor = GenericJobBatchDescriber; 9 | export type JobDescriptor = GenericJobDescriber; 10 | -------------------------------------------------------------------------------- /src/commands/run/questions/param-questions.model.ts: -------------------------------------------------------------------------------- 1 | export interface ParamsResult { 2 | branch?: string; 3 | adEngineVersion?: string; 4 | sandbox?: string; 5 | configBranch?: string; 6 | datacenter?: string; 7 | crowdinBranch?: string; 8 | debug?: boolean; 9 | testBranch?: string; 10 | query?: string; 11 | fandomEnvironment?: string; 12 | extension?: string; 13 | name?: string; 14 | platformsBranch?: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/filter-list.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters list and return result of fallback if empty. 3 | */ 4 | export async function filterList( 5 | input: string[], 6 | available: T[], 7 | fallback: () => Promise, 8 | ): Promise { 9 | const filtered: T[] = available.filter(item => input.includes(item)); 10 | 11 | if (!filtered.length) { 12 | return await fallback(); 13 | } 14 | 15 | return filtered; 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/print-header.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import * as clear from 'clear'; 3 | import { textSync } from 'figlet'; 4 | import { packageJson } from './package'; 5 | 6 | export function printHeader(): void { 7 | clear(); 8 | 9 | if (packageJson.version === '0.0.0-development') { 10 | console.log(chalk.red(textSync('AdEng Jenkins', { horizontalLayout: 'full' }))); 11 | console.log(chalk.red('You are in dev mode!', '\n')); 12 | } else { 13 | console.log(chalk.yellow(textSync('AdEng Jenkins', { horizontalLayout: 'full' })), '\n'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | cache: 4 | directories: 5 | - '$HOME/.npm' 6 | 7 | notifications: 8 | email: false 9 | 10 | install: npm ci 11 | 12 | jobs: 13 | include: 14 | - stage: 'Test' 15 | node_js: lts/* 16 | script: 17 | - npm run build 18 | - npm run test:prod && npm run report-coverage 19 | 20 | - stage: 'Release' 21 | if: branch = master AND type = push 22 | node_js: lts/* 23 | script: skip 24 | deploy: 25 | provider: script 26 | skip_cleanup: true 27 | script: npx semantic-release 28 | on: 29 | branch: master 30 | -------------------------------------------------------------------------------- /src/utils/sandbox.ts: -------------------------------------------------------------------------------- 1 | export const sandboxes: Sandbox[] = [ 2 | 'sandbox-adeng01', 3 | 'sandbox-adeng02', 4 | 'sandbox-adeng03', 5 | 'sandbox-adeng04', 6 | 'sandbox-adeng05', 7 | 'sandbox-adeng06', 8 | 'sandbox-adeng07', 9 | 'sandbox-adeng08', 10 | ]; 11 | 12 | export type Sandbox = 13 | | 'sandbox-adeng01' 14 | | 'sandbox-adeng02' 15 | | 'sandbox-adeng03' 16 | | 'sandbox-adeng04' 17 | | 'sandbox-adeng05' 18 | | 'sandbox-adeng06' 19 | | 'sandbox-adeng07' 20 | | 'sandbox-adeng08'; 21 | 22 | export function isSandbox(input: string): input is Sandbox { 23 | return sandboxes.includes(input as Sandbox); 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/sandbox/index.ts: -------------------------------------------------------------------------------- 1 | import * as inquirer from 'inquirer'; 2 | import { isSandbox, Sandbox, sandboxes } from '../../utils/sandbox'; 3 | import { store } from '../../utils/store'; 4 | import { requiredInput } from '../run/questions/question-helpers'; 5 | 6 | export async function sandbox(input: string) { 7 | if (isSandbox(input)) { 8 | store.sandbox = input; 9 | } else { 10 | const result = await inquirer.prompt<{ sandbox: Sandbox }>(question); 11 | store.sandbox = result.sandbox; 12 | console.log(''); 13 | } 14 | } 15 | 16 | const question: inquirer.Question = { 17 | name: 'sandbox', 18 | type: 'list', 19 | message: 'Choose your default sandbox', 20 | validate: requiredInput, 21 | choices: sandboxes, 22 | pageSize: sandboxes.length, 23 | default: store.sandbox, 24 | }; 25 | -------------------------------------------------------------------------------- /src/utils/store.ts: -------------------------------------------------------------------------------- 1 | import * as Configstore from 'configstore'; 2 | import { packageJson } from './package'; 3 | import { Sandbox } from './sandbox'; 4 | 5 | export class Store { 6 | private state = new Configstore(packageJson.name); 7 | 8 | get sandbox(): Sandbox { 9 | return this.state.get('sandbox'); 10 | } 11 | 12 | set sandbox(input: Sandbox) { 13 | this.state.set('sandbox', input); 14 | } 15 | 16 | get username(): string { 17 | return this.state.get('username'); 18 | } 19 | 20 | set username(input: string) { 21 | this.state.set('username', input); 22 | } 23 | 24 | get token(): string { 25 | return this.state.get('token'); 26 | } 27 | 28 | set token(input: string) { 29 | this.state.set('token', input); 30 | } 31 | } 32 | 33 | export const store = new Store(); 34 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "ordered-imports": [ 5 | true, 6 | { 7 | "named-imports-order": "case-insensitive", 8 | "import-sources-order": "case-insensitive" 9 | } 10 | ], 11 | "member-ordering": [ 12 | true, 13 | { 14 | "order": [ 15 | "static-field", 16 | "static-method", 17 | "instance-field", 18 | "constructor", 19 | "instance-method" 20 | ] 21 | } 22 | ], 23 | "interface-name": [true, "never-prefix"], 24 | "member-access": [true, "no-public"], 25 | "array-type": [true, "array"], 26 | "no-console": false, 27 | "no-var-requires": false, 28 | "no-unused-expression": false, 29 | "object-literal-sort-keys": false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/run/questions/job-questions.ts: -------------------------------------------------------------------------------- 1 | import * as inquirer from 'inquirer'; 2 | import { filterList } from '../../../utils/filter-list'; 3 | 4 | export type Job = 'update' | 'deploy' | 'test'; 5 | export const availableJobs: Job[] = ['update', 'deploy', 'test']; 6 | 7 | export async function verifyJobs(jobs: string[]): Promise { 8 | return filterList(jobs, availableJobs, () => askForFobs()); 9 | } 10 | 11 | async function askForFobs(): Promise { 12 | const questions: inquirer.Questions = [ 13 | { 14 | name: 'jobs', 15 | type: 'checkbox', 16 | message: 'Choose jobs to execute', 17 | validate(value) { 18 | if (value.length) { 19 | return true; 20 | } else { 21 | return 'Please choose at least one job.'; 22 | } 23 | }, 24 | choices: availableJobs, 25 | }, 26 | ]; 27 | const result: any = await inquirer.prompt(questions); 28 | 29 | return result.jobs; 30 | } 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We're really glad you're reading this, because we need volunteer developers to help this project come to fruition. 👏 2 | 3 | ## Instructions 4 | 5 | These steps will guide you through contributing to this project: 6 | 7 | - Fork the repo 8 | - Clone it and install dependencies 9 | 10 | git clone https://github.com/Bielik20/adeng-jenkins-cli 11 | npm install 12 | 13 | Keep in mind that after running `npm install` the git repo is reset. So a good way to cope with this is to have a copy of the folder to push the changes, and the other to try them. 14 | 15 | Make and commit your changes. Make sure the commands npm run build and npm run test:prod are working. 16 | 17 | Finally send a [GitHub Pull Request](https://github.com/Bielik20/adeng-jenkins-cli/compare?expand=1) with a clear list of what you've done (read more [about pull requests](https://help.github.com/articles/about-pull-requests/)). Make sure all of your commits are atomic (one feature per commit). 18 | -------------------------------------------------------------------------------- /src/commands/run/questions/question-helpers.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import * as branch from 'git-branch'; 3 | 4 | export function adenToUpper(input: string): string { 5 | if (input.indexOf('aden-') === 0) { 6 | return `ADEN-${input.slice(5)}`; 7 | } 8 | 9 | return input; 10 | } 11 | 12 | export function requiredInput(input?: string): boolean | string { 13 | if (!!input) { 14 | return true; 15 | } else { 16 | return 'This file is required.'; 17 | } 18 | } 19 | 20 | export function currentBranch(): string | undefined { 21 | try { 22 | return branch.sync(); 23 | } catch (e) { 24 | return undefined; 25 | } 26 | } 27 | 28 | export async function replaceLatestWithAdEngineVersion(input: string) { 29 | if (input !== 'latest') { 30 | return input; 31 | } 32 | 33 | const response: any = await axios.get( 34 | 'https://raw.githubusercontent.com/Wikia/ad-engine/dev/package.json', 35 | ); 36 | 37 | return `v${response.data.version}`; 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/jenkins.ts: -------------------------------------------------------------------------------- 1 | import * as createJenkins from 'jenkins'; 2 | import { JenkinsPromisifiedAPI } from 'jenkins'; 3 | import { JenkinsRxJs } from 'jenkins-rxjs'; 4 | import { ensureAuthenticated } from '../commands/login'; 5 | import { store } from './store'; 6 | 7 | export abstract class Jenkins { 8 | private static promisified: JenkinsPromisifiedAPI; 9 | private static rxjs: JenkinsRxJs; 10 | 11 | static async getJenkinsPromisified(): Promise { 12 | if (!Jenkins.promisified) { 13 | await ensureAuthenticated(); 14 | Jenkins.promisified = createJenkins({ 15 | baseUrl: `http://${store.username}:${store.token}@jenkins.wikia-prod:8080`, 16 | promisify: true, 17 | }); 18 | } 19 | 20 | return Jenkins.promisified; 21 | } 22 | 23 | static async getJenkinsRxJs() { 24 | if (!Jenkins.rxjs) { 25 | const jenkinsPromise = await Jenkins.getJenkinsPromisified(); 26 | 27 | Jenkins.rxjs = new JenkinsRxJs(jenkinsPromise); 28 | } 29 | 30 | return Jenkins.rxjs; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/run/questions/project-questions.ts: -------------------------------------------------------------------------------- 1 | import * as inquirer from 'inquirer'; 2 | import { filterList } from '../../../utils/filter-list'; 3 | 4 | export type Project = 'ucp' | 'app' | 'mobile-wiki' | 'f2' | 'platforms'; 5 | export const availableProjects: Project[] = ['ucp', 'app', 'mobile-wiki', 'f2', 'platforms']; 6 | 7 | export async function verifyProjects(projects: string[]): Promise { 8 | return filterList(projects, availableProjects, () => askForProjects()); 9 | } 10 | 11 | async function askForProjects(): Promise { 12 | const questions: inquirer.Questions = [ 13 | { 14 | name: 'projects', 15 | type: 'checkbox', 16 | message: 'Choose project to include', 17 | validate(value) { 18 | if (value.length) { 19 | return true; 20 | } else { 21 | return 'Please choose at least one project.'; 22 | } 23 | }, 24 | choices: availableProjects, 25 | default: ['app', 'mobile-wiki'], 26 | }, 27 | ]; 28 | const result: any = await inquirer.prompt(questions); 29 | 30 | return result.projects; 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2020 Damian Bielecki 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | .idea 65 | dist/ 66 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | .idea 65 | src/ 66 | -------------------------------------------------------------------------------- /src/commands/login/index.ts: -------------------------------------------------------------------------------- 1 | import * as ansiEscapes from 'ansi-escapes'; 2 | import * as inquirer from 'inquirer'; 3 | import { store } from '../../utils/store'; 4 | import { requiredInput } from '../run/questions/question-helpers'; 5 | 6 | export async function login() { 7 | const { username, token } = await inquirer.prompt<{ username: string; token: string }>(questions); 8 | store.username = username; 9 | store.token = token; 10 | console.log(''); 11 | } 12 | 13 | export function isAuthenticated(): boolean { 14 | return !!store.username && !!store.token; 15 | } 16 | 17 | export async function ensureAuthenticated(): Promise { 18 | if (isAuthenticated()) { 19 | return; 20 | } 21 | 22 | console.log('You need to login first'); 23 | await login(); 24 | } 25 | 26 | const questions: inquirer.Questions = [ 27 | { 28 | name: 'username', 29 | message: 'Your Jenkins user name', 30 | validate: requiredInput, 31 | default: store.username, 32 | }, 33 | { 34 | name: 'token', 35 | message: `Your Jenkins API token (${ansiEscapes.link( 36 | 'help', 37 | 'https://stackoverflow.com/questions/45466090/how-to-get-the-api-token-for-jenkins', 38 | )})`, 39 | validate: requiredInput, 40 | default: store.token, 41 | }, 42 | ]; 43 | -------------------------------------------------------------------------------- /src/commands/run/index.ts: -------------------------------------------------------------------------------- 1 | import * as ansiEscapes from 'ansi-escapes'; 2 | import { JobBatchRunner, uiManagerSimulator } from 'jenkins-jobs-runner'; 3 | import { Jenkins } from '../../utils/jenkins'; 4 | import { JobsBuilder } from './jobs-builder'; 5 | import { JobBatchDescriptor } from './jobs-builder/models'; 6 | import { Job, ParamsResult, Project, promptParams, verifyJobs, verifyProjects } from './questions'; 7 | 8 | export async function run(inputJobs: string[], inputProjects: string[], extended: boolean) { 9 | // await uiManagerSimulator(); 10 | questionnaire(inputJobs, inputProjects, extended); 11 | } 12 | 13 | async function questionnaire(inputJobs: string[], inputProjects: string[], extended: boolean) { 14 | const jenkinsRxJs = await Jenkins.getJenkinsRxJs(); 15 | 16 | const jobs: Job[] = await verifyJobs(inputJobs); 17 | const projects: Project[] = await verifyProjects(inputProjects); 18 | const params: ParamsResult = await promptParams(jobs, projects, extended); 19 | 20 | process.stdout.write(ansiEscapes.cursorDown(1) + ansiEscapes.cursorLeft); 21 | 22 | const builder = new JobsBuilder(); 23 | const builderResult: JobBatchDescriptor[] = builder.build(jobs, projects, params); 24 | 25 | const runner = new JobBatchRunner(jenkinsRxJs); 26 | await runner.runBatches(builderResult); 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/run/jobs-builder/update-job-builder.ts: -------------------------------------------------------------------------------- 1 | import { ParamsResult } from '../questions/param-questions.model'; 2 | import { Project } from '../questions/project-questions'; 3 | import { JobDescriptor } from './models'; 4 | 5 | interface UpdateJobParams { 6 | branch: string; 7 | adengine_version: string; 8 | } 9 | 10 | export class UpdateJobBuilder { 11 | private projectNameMap = new Map([ 12 | ['app', 'update_dependencies_app'], 13 | ['mobile-wiki', 'update_dependencies_mobilewiki'], 14 | ['f2', 'update_dependencies_f2'], 15 | ]); 16 | 17 | build(projects: Project[], params: ParamsResult): JobDescriptor[] { 18 | const parameters: UpdateJobParams = this.mapProjectParams(params); 19 | 20 | return projects 21 | .filter((project: Project) => this.projectNameMap.has(project)) 22 | .map((project: Project) => ({ 23 | displayName: project, 24 | opts: { 25 | name: this.mapProjectName(project), 26 | parameters, 27 | }, 28 | })); 29 | } 30 | 31 | private mapProjectName(input: Project): string { 32 | return this.projectNameMap.get(input); 33 | } 34 | 35 | private mapProjectParams(input: ParamsResult): UpdateJobParams { 36 | return { 37 | branch: input.branch, 38 | adengine_version: input.adEngineVersion, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as program from 'commander'; 2 | import { login } from './commands/login'; 3 | import { run } from './commands/run'; 4 | import { sandbox } from './commands/sandbox'; 5 | import { checkVersion } from './utils/check-version'; 6 | import { list } from './utils/list'; 7 | import { packageJson } from './utils/package'; 8 | import { printHeader } from './utils/print-header'; 9 | 10 | export async function start() { 11 | printHeader(); 12 | checkVersion(); 13 | 14 | program.version(packageJson.version).description(packageJson.description); 15 | program 16 | .command('run') 17 | .alias('r') 18 | .description('Run Jenkins jobs') 19 | .option('-j, --jobs ', 'Jenkins jobs to run', list, []) 20 | .option('-p, --projects ', 'Project to include', list, []) 21 | .option('-e, --extended', 'Whether to show extended options', false) 22 | .action(({ jobs, projects, extended }) => run(jobs, projects, extended)); 23 | program 24 | .command('sandbox') 25 | .alias('s') 26 | .description('Choose your default sandbox') 27 | .option('name', 'Name of the sandbox') 28 | .action(input => sandbox(input)); 29 | program 30 | .command('login') 31 | .alias('l') 32 | .description('Login to Jenkins') 33 | .action(() => login()); 34 | 35 | if (process.argv.length === 2) { 36 | process.argv.push('-h'); 37 | } 38 | 39 | program.parse(process.argv); 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/run/jobs-builder/test-job-builder.ts: -------------------------------------------------------------------------------- 1 | import { ParamsResult } from '../questions/param-questions.model'; 2 | import { Project } from '../questions/project-questions'; 3 | import { JobDescriptor } from './models'; 4 | 5 | interface TestJobParams { 6 | env: string; 7 | branch: string; 8 | querystring: string; 9 | 'fandom-env': string; 10 | extension: string; 11 | 'custom-name': string; 12 | 'tabs-to-trigger': string; 13 | } 14 | 15 | export class TestJobBuilder { 16 | private projectNameMap = new Map([ 17 | ['app', 'ads-app-preview'], 18 | ['mobile-wiki', 'ads-mobile-wiki-preview'], 19 | ['f2', 'ads-news-and-stories-prod'], 20 | ]); 21 | 22 | build(projects: Project[], params: ParamsResult): JobDescriptor[] { 23 | const result: JobDescriptor = { 24 | displayName: projects.filter(project => this.projectNameMap.has(project)).join(', ') as any, 25 | opts: { 26 | name: 'ads-synthetic-run', 27 | parameters: this.mapParams(projects, params), 28 | }, 29 | }; 30 | 31 | return [result]; 32 | } 33 | 34 | private mapParams(projects: Project[], params: ParamsResult): TestJobParams { 35 | return { 36 | env: params.sandbox, 37 | branch: this.ensureOrigin(params.testBranch), 38 | querystring: params.query, 39 | 'fandom-env': params.fandomEnvironment, 40 | extension: params.extension, 41 | 'custom-name': params.name, 42 | 'tabs-to-trigger': this.mapTagsToTrigger(projects), 43 | }; 44 | } 45 | 46 | private ensureOrigin(branch?: string): string { 47 | if (!branch || branch.startsWith('origin')) { 48 | return branch; 49 | } 50 | 51 | return `origin/${branch}`; 52 | } 53 | 54 | private mapTagsToTrigger(projects: Project[]): string { 55 | return projects 56 | .map((project: Project) => this.projectNameMap.get(project)) 57 | .filter((value: string) => !!value) 58 | .join(' '); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/run/jobs-builder/jobs-builder.ts: -------------------------------------------------------------------------------- 1 | import { Job } from '../questions/job-questions'; 2 | import { ParamsResult } from '../questions/param-questions.model'; 3 | import { Project } from '../questions/project-questions'; 4 | import { DeployJobBuilder } from './deploy-job-builder'; 5 | import { JobBatchDescriptor, JobDescriptor } from './models'; 6 | import { TestJobBuilder } from './test-job-builder'; 7 | import { UpdateJobBuilder } from './update-job-builder'; 8 | 9 | export class JobsBuilder { 10 | private updateJobBuilder = new UpdateJobBuilder(); 11 | private deployJobBuilder = new DeployJobBuilder(); 12 | private testJobBuilder = new TestJobBuilder(); 13 | 14 | build(jobs: Job[], projects: Project[], params: ParamsResult): JobBatchDescriptor[] { 15 | const batchDescriptors: JobBatchDescriptor[] = []; 16 | 17 | if (jobs.includes('update')) { 18 | const jobDescriptors: JobDescriptor[] = this.updateJobBuilder.build(projects, params); 19 | 20 | if (jobDescriptors.length) { 21 | batchDescriptors.push({ 22 | displayName: 'update', 23 | jobDescriptor: jobDescriptors, 24 | }); 25 | } 26 | } 27 | 28 | if (jobs.includes('deploy')) { 29 | const jobDescriptors: JobDescriptor[] = this.deployJobBuilder.build(projects, params); 30 | 31 | if (jobDescriptors.length) { 32 | batchDescriptors.push({ 33 | displayName: 'deploy', 34 | jobDescriptor: jobDescriptors, 35 | }); 36 | } 37 | } 38 | 39 | if (jobs.includes('test')) { 40 | const jobDescriptors: JobDescriptor[] = this.testJobBuilder.build(projects, params); 41 | 42 | if ( 43 | jobDescriptors.some(jobDescriptor => !!jobDescriptor.opts.parameters['tabs-to-trigger']) 44 | ) { 45 | batchDescriptors.push({ 46 | displayName: 'test', 47 | jobDescriptor: jobDescriptors, 48 | }); 49 | } 50 | } 51 | 52 | return batchDescriptors; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AdEng Jenkins CLI 2 | 3 |

4 | 5 | npm version 6 | 7 | 8 | npm downloads 9 | 10 | 11 | Travis 12 | 13 | 14 | Travis 15 | 16 |

17 | 18 |

19 | 20 | Travis 21 | 22 | 23 | Travis 24 | 25 | 26 | Travis 27 | 28 |

29 | 30 | Base: 31 | 32 | - https://github.com/Bielik20/outside-cli 33 | - https://github.com/Bielik20/git-cli 34 | 35 | Resources: 36 | 37 | - https://github.com/sindresorhus/awesome-nodejs 38 | 39 | ## How to install 40 | 41 | - `npm i -g adeng-jenkins-cli` 42 | 43 | ## How to use 44 | 45 | - `jenkins -h` <- it will give you list of commands 46 | 47 | ## Development Guide 48 | 49 | - `npm ci` 50 | - `npm link` - it will link package so it will be available globally 51 | - `npm run watch` - it will compile files to dist directory 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adeng-jenkins-cli", 3 | "version": "0.0.0-development", 4 | "description": "A CLI app that helps you deal with jenkins.", 5 | "license": "MIT", 6 | "homepage": "https://github.com/Bielik20/adeng-jenkins-cli#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Bielik20/adeng-jenkins-cli.git" 10 | }, 11 | "engines": { 12 | "node": ">=8" 13 | }, 14 | "preferGlobal": true, 15 | "bin": { 16 | "jenkins": "bin/jenkins" 17 | }, 18 | "author": "Damian Bielecki ", 19 | "scripts": { 20 | "prepublishOnly": "npm run build", 21 | "prebuild": "rimraf dist", 22 | "build": "tsc", 23 | "watch": "npm run build -- --watch", 24 | "lint": "tslint --project .", 25 | "lint:fix": "tslint --fix --project .", 26 | "prettier": "prettier --write 'src/**/*.{ts,js,json,css,scss,html}'", 27 | "format": "npm run prettier && npm run lint:fix", 28 | "test": "jest --coverage", 29 | "test:watch": "jest --coverage --watch", 30 | "test:prod": "npm run lint && npm run test -- --no-cache", 31 | "report-coverage": "cat ./coverage/lcov.info | coveralls", 32 | "commit": "git-cz", 33 | "semantic-release": "semantic-release" 34 | }, 35 | "dependencies": { 36 | "ansi-escapes": "^4.1.0", 37 | "axios": "^0.18.1", 38 | "boxen": "^3.1.0", 39 | "chalk": "^2.4.2", 40 | "clear": "^0.1.0", 41 | "clui": "^0.3.6", 42 | "commander": "^2.19.0", 43 | "configstore": "^4.0.0", 44 | "figlet": "^1.2.1", 45 | "git-branch": "^2.0.1", 46 | "inquirer": "^6.2.2", 47 | "jenkins": "^0.25.0", 48 | "jenkins-jobs-runner": "^1.0.3", 49 | "jenkins-rxjs": "^1.1.1", 50 | "log-symbols": "^2.2.0", 51 | "minimist": "^1.2.0", 52 | "multi-progress": "^2.0.0", 53 | "rxjs": "^6.5.2", 54 | "update-notifier": "2.5.0" 55 | }, 56 | "devDependencies": { 57 | "@commitlint/cli": "^8.3.5", 58 | "@commitlint/config-conventional": "^8.3.4", 59 | "@types/configstore": "^4.0.0", 60 | "@types/figlet": "^1.2.0", 61 | "@types/inquirer": "^6.0.0", 62 | "@types/jenkins": "^0.23.1", 63 | "@types/jest": "^25.1.3", 64 | "@types/minimist": "^1.2.0", 65 | "@types/multi-progress": "^2.0.3", 66 | "@types/node": "^11.12.0", 67 | "@types/progress": "^2.0.3", 68 | "@types/update-notifier": "2.5.0", 69 | "commitizen": "^4.0.3", 70 | "coveralls": "^3.0.9", 71 | "cz-conventional-changelog": "^3.1.0", 72 | "husky": "^1.3.1", 73 | "jest": "^25.1.0", 74 | "lint-staged": "^10.0.7", 75 | "prettier": "^1.16.4", 76 | "rimraf": "^3.0.2", 77 | "semantic-release": "^17.0.4", 78 | "ts-jest": "^25.2.1", 79 | "tslint": "^5.14.0", 80 | "tslint-config-prettier": "^1.18.0", 81 | "typescript": "^3.8.2" 82 | }, 83 | "config": { 84 | "commitizen": { 85 | "path": "cz-conventional-changelog" 86 | } 87 | }, 88 | "commitlint": { 89 | "extends": [ 90 | "@commitlint/config-conventional" 91 | ] 92 | }, 93 | "husky": { 94 | "hooks": { 95 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 96 | "pre-commit": "lint-staged" 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/commands/run/jobs-builder/deploy-job-builder.ts: -------------------------------------------------------------------------------- 1 | import { ParamsResult } from '../questions/param-questions.model'; 2 | import { Project } from '../questions/project-questions'; 3 | import { JobDescriptor } from './models'; 4 | 5 | interface DeployJobAppAndUcpParams { 6 | sandbox: string; 7 | ucp_branch: string; 8 | app_branch: string; 9 | config_branch: string; 10 | datacenter: string; 11 | crowdin_branch: string; 12 | debug: boolean; 13 | } 14 | 15 | interface DeployJobMobileWikiParams { 16 | sandbox: string; 17 | branch: string; 18 | dc: string; 19 | crowdin_branch: string; 20 | } 21 | 22 | interface DeployPlatformsParams { 23 | BRANCH: string; 24 | } 25 | 26 | type DeployProject = Project | 'app-ucp'; 27 | 28 | export class DeployJobBuilder { 29 | private projectNameMap = new Map([ 30 | ['app-ucp', 'mediawiki-deploy-sandbox-ucp'], 31 | ['ucp', 'mediawiki-deploy-sandbox-ucp'], 32 | ['app', 'mediawiki-deploy-sandbox-ucp'], 33 | ['mobile-wiki', 'mobile-wiki-deploy-sandbox'], 34 | ['platforms', 'ad_engine_platforms_deploy_branch'], 35 | ]); 36 | 37 | build(projects: Project[], params: ParamsResult): JobDescriptor[] { 38 | const deployProjects = this.parseDeployProject(projects); 39 | 40 | return deployProjects 41 | .filter((project: Project) => this.projectNameMap.has(project)) 42 | .map((project: Project) => ({ 43 | displayName: project, 44 | opts: { 45 | name: this.mapProjectName(project), 46 | parameters: this.mapProjectParams(project, params), 47 | }, 48 | })); 49 | } 50 | 51 | private parseDeployProject(projects: Project[]): DeployProject[] { 52 | if (projects.includes('app') && projects.includes('ucp')) { 53 | return ['app-ucp', ...projects.filter(project => !['app', 'ucp'].includes(project))]; 54 | } 55 | return projects; 56 | } 57 | 58 | private mapProjectName(input: Project): string { 59 | return this.projectNameMap.get(input); 60 | } 61 | 62 | private mapProjectParams( 63 | project: DeployProject, 64 | input: ParamsResult, 65 | ): DeployJobAppAndUcpParams | DeployJobMobileWikiParams | DeployPlatformsParams { 66 | switch (project) { 67 | case 'app-ucp': 68 | return { 69 | sandbox: input.sandbox, 70 | ucp_branch: input.branch, 71 | app_branch: input.branch, 72 | config_branch: input.configBranch, 73 | datacenter: input.datacenter, 74 | crowdin_branch: input.crowdinBranch, 75 | debug: input.debug, 76 | }; 77 | 78 | case 'ucp': 79 | return { 80 | sandbox: input.sandbox, 81 | ucp_branch: input.branch, 82 | app_branch: 'dev', 83 | config_branch: input.configBranch, 84 | datacenter: input.datacenter, 85 | crowdin_branch: input.crowdinBranch, 86 | debug: input.debug, 87 | }; 88 | 89 | case 'app': 90 | return { 91 | sandbox: input.sandbox, 92 | ucp_branch: 'master', 93 | app_branch: input.branch, 94 | config_branch: input.configBranch, 95 | datacenter: input.datacenter, 96 | crowdin_branch: input.crowdinBranch, 97 | debug: input.debug, 98 | }; 99 | 100 | case 'mobile-wiki': 101 | return { 102 | sandbox: input.sandbox, 103 | branch: input.branch, 104 | dc: input.datacenter, 105 | crowdin_branch: input.crowdinBranch, 106 | }; 107 | 108 | case 'platforms': 109 | return { 110 | BRANCH: input.platformsBranch, 111 | }; 112 | } 113 | 114 | console.log('Aborting - Trying to build DeployJob with unknown Project'); 115 | process.exit(1); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/commands/run/questions/param-questions.ts: -------------------------------------------------------------------------------- 1 | import * as inquirer from 'inquirer'; 2 | import { sandboxes } from '../../../utils/sandbox'; 3 | import { store } from '../../../utils/store'; 4 | import { Job } from './job-questions'; 5 | import { ParamsResult } from './param-questions.model'; 6 | import { availableProjects, Project } from './project-questions'; 7 | import { 8 | adenToUpper, 9 | currentBranch, 10 | replaceLatestWithAdEngineVersion, 11 | requiredInput, 12 | } from './question-helpers'; 13 | 14 | interface FilterParamQuestion extends inquirer.Question { 15 | name: keyof ParamsResult; 16 | destined: { 17 | jobs: Job[]; 18 | projects: Project[]; 19 | extended: boolean; 20 | }; 21 | } 22 | 23 | export async function promptParams( 24 | jobs: Job[], 25 | projects: Project[], 26 | extended: boolean, 27 | ): Promise { 28 | const paramQuestions: inquirer.Questions = getParamQuestions(jobs, projects, extended); 29 | const result: ParamsResult = await inquirer.prompt(paramQuestions); 30 | 31 | result.datacenter = result.datacenter || 'sjc'; 32 | result.debug = typeof result.debug === 'boolean' ? result.debug : true; 33 | result.fandomEnvironment = result.fandomEnvironment || 'sandbox-adeng'; 34 | result.configBranch = result.configBranch || 'dev'; 35 | 36 | return result; 37 | } 38 | 39 | function getParamQuestions( 40 | jobs: Job[], 41 | projects: Project[], 42 | extended: boolean, 43 | ): inquirer.Questions { 44 | return questions.filter(question => { 45 | const isJobOk = question.destined.jobs.some(job => jobs.includes(job)); 46 | const isProjectOk = question.destined.projects.some(project => projects.includes(project)); 47 | const isExtendedOk = !question.destined.extended || extended; 48 | 49 | return isJobOk && isProjectOk && isExtendedOk; 50 | }); 51 | } 52 | 53 | const questions: FilterParamQuestion[] = [ 54 | { 55 | name: 'branch', 56 | message: 'Project branch', 57 | validate: requiredInput, 58 | filter: adenToUpper, 59 | transformer: adenToUpper, 60 | default: currentBranch, 61 | destined: { 62 | jobs: ['update', 'deploy'], 63 | projects: availableProjects.filter(project => project !== 'platforms'), 64 | extended: false, 65 | }, 66 | }, 67 | { 68 | name: 'adEngineVersion', 69 | message: 'Version of @wikia/ad-engine (can be "latest")', 70 | validate: requiredInput, 71 | filter: replaceLatestWithAdEngineVersion, 72 | default: (answers: ParamsResult) => answers.branch, 73 | destined: { 74 | jobs: ['update'], 75 | projects: availableProjects, 76 | extended: false, 77 | }, 78 | }, 79 | { 80 | name: 'sandbox', 81 | type: 'list', 82 | message: 'Sandbox', 83 | validate: requiredInput, 84 | choices: sandboxes, 85 | pageSize: sandboxes.length, 86 | default: store.sandbox, 87 | destined: { 88 | jobs: ['deploy', 'test'], 89 | projects: ['ucp', 'app', 'mobile-wiki', 'f2'], 90 | extended: false, 91 | }, 92 | }, 93 | { 94 | name: 'configBranch', 95 | message: 'Config branch e.g. release-01, PLAT-345', 96 | default: 'dev', 97 | destined: { 98 | jobs: ['update'], 99 | projects: ['app'], 100 | extended: true, 101 | }, 102 | }, 103 | { 104 | name: 'datacenter', 105 | message: 'Datacenter', 106 | validate: requiredInput, 107 | default: 'sjc', 108 | destined: { 109 | jobs: ['update'], 110 | projects: ['app', 'mobile-wiki'], 111 | extended: true, 112 | }, 113 | }, 114 | { 115 | name: 'crowdinBranch', 116 | message: 'Branch for Crowdin translations (leave empty if translations update not needed)', 117 | destined: { 118 | jobs: ['update'], 119 | projects: ['app', 'mobile-wiki'], 120 | extended: true, 121 | }, 122 | }, 123 | { 124 | name: 'debug', 125 | type: 'confirm', 126 | message: 'Branch for Crowdin translations (leave empty if translations update not needed)', 127 | default: true, 128 | destined: { 129 | jobs: ['update'], 130 | projects: ['app'], 131 | extended: true, 132 | }, 133 | }, 134 | { 135 | name: 'testBranch', 136 | message: 'Branch for Tests', 137 | destined: { 138 | jobs: ['test'], 139 | projects: ['app', 'mobile-wiki', 'f2'], 140 | extended: true, 141 | }, 142 | }, 143 | { 144 | name: 'query', 145 | message: 'Url params', 146 | default: `cb=${+new Date()}`, 147 | destined: { 148 | jobs: ['test'], 149 | projects: ['app', 'mobile-wiki', 'f2'], 150 | extended: false, 151 | }, 152 | }, 153 | { 154 | name: 'fandomEnvironment', 155 | message: 'Environment for Fandom ( Upstream) tests', 156 | default: 'sandbox-adeng', 157 | destined: { 158 | jobs: ['test'], 159 | projects: ['app', 'mobile-wiki', 'f2'], 160 | extended: true, 161 | }, 162 | }, 163 | { 164 | name: 'extension', 165 | message: 'Additional browser extenstions e.g. adblock', 166 | destined: { 167 | jobs: ['test'], 168 | projects: ['app', 'mobile-wiki', 'f2'], 169 | extended: true, 170 | }, 171 | }, 172 | { 173 | name: 'name', 174 | message: 'Custom name which will be added to tab name', 175 | validate: requiredInput, 176 | default: (answers: ParamsResult) => { 177 | const userInitials: string = store.username.slice(0, 2); 178 | 179 | return answers.branch 180 | ? `${answers.branch}-${userInitials}` 181 | : `${answers.sandbox}-${userInitials}`; 182 | }, 183 | destined: { 184 | jobs: ['test'], 185 | projects: ['app', 'mobile-wiki', 'f2'], 186 | extended: false, 187 | }, 188 | }, 189 | { 190 | name: 'platformsBranch', 191 | message: 'Platform branch', 192 | validate: requiredInput, 193 | filter: adenToUpper, 194 | transformer: adenToUpper, 195 | destined: { 196 | jobs: ['deploy'], 197 | projects: ['platforms'], 198 | extended: false, 199 | }, 200 | }, 201 | ]; 202 | --------------------------------------------------------------------------------