├── .vscode ├── settings.json └── tasks.json ├── bin └── ci-npm-update ├── doc-assets └── ci-npm-update-pr.png ├── .gitignore ├── test ├── tsconfig.json ├── support │ ├── mocha-setup-node.js │ └── default.opts ├── fixture │ ├── package-lock.json │ └── package-lock-updated.json ├── compare_view.test.ts ├── package_lock.test.ts └── github.test.ts ├── circle.yml ├── .npmignore ├── tsconfig.json ├── tslint.json ├── app.json ├── CHANGES.md ├── package.json ├── src ├── compare_view.ts ├── cli.ts ├── issue.ts ├── npm_config.ts ├── package_lock.ts ├── github.ts └── index.ts ├── README.md └── LICENSE.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib/" 3 | } -------------------------------------------------------------------------------- /bin/ci-npm-update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("source-map-support/register"); 3 | require("../lib/cli"); 4 | -------------------------------------------------------------------------------- /doc-assets/ci-npm-update-pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitjourney/ci-npm-update/HEAD/doc-assets/ci-npm-update-pr.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.log 3 | 4 | node_modules/ 5 | typings/ 6 | *.build/ 7 | lib/ 8 | .vscode/*.db 9 | .vscode/*.db-* 10 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "outDir": "../test.build", 5 | "types": ["node", "mocha"] 6 | }, 7 | "exclude": [ 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/support/mocha-setup-node.js: -------------------------------------------------------------------------------- 1 | //inject mocha globally to allow custom interface refer without direct import - bypass bundle issue 2 | global.mocha = require('mocha'); 3 | global.Suite = global.mocha.Suite; 4 | global.Test = global.mocha.Test; -------------------------------------------------------------------------------- /test/support/default.opts: -------------------------------------------------------------------------------- 1 | --compilers ts:espower-typescript/guess 2 | 3 | --require source-map-support/register 4 | --require ./test/support/mocha-setup-node.js 5 | 6 | --full-trace 7 | --check-leaks 8 | 9 | --recursive 10 | --timeout 2000 11 | -------------------------------------------------------------------------------- /test/fixture/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ci-npm-update", 3 | "version": "1.0.9", 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "ts-node": { 7 | "version": "1.7.3", 8 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-1.7.3.tgz", 9 | "integrity": "sha1-3uf4qEdRcy08Lkl8rFoC+xF9/uc=", 10 | "dev": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixture/package-lock-updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ci-npm-update", 3 | "version": "1.0.9", 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "ts-node": { 7 | "version": "3.1.0", 8 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.1.0.tgz", 9 | "integrity": "sha1-p17FrrSPMFixuUXbp2XxFQuoj4w=", 10 | "dev": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6 4 | 5 | dependencies: 6 | pre: 7 | - npm install -g npm@5 8 | 9 | compile: 10 | override: 11 | - npm run build 12 | 13 | test: 14 | post: 15 | - bin/ci-npm-update 16 | 17 | deployment: 18 | update-dependencies: 19 | branch: master 20 | commands: 21 | - > 22 | if [ -n "${NPM_UPDATE}" ] ; then 23 | bin/ci-npm-update --execute 24 | fi 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | 3 | # for editor settings 4 | .idea/ 5 | .vscode 6 | 7 | # for npm 8 | node_modules/ 9 | npm-shrinkwrap.json 10 | 11 | # for typings 12 | typings/ 13 | 14 | # Don't include ts files 15 | # for https://github.com/Microsoft/TypeScript/issues/4667 16 | src/ 17 | 18 | # tests 19 | test/ 20 | 21 | # doc 22 | README.md 23 | CHANGES.md 24 | doc-assets/ 25 | 26 | # Heroku 27 | build-circleci 28 | app.json 29 | 30 | # misc 31 | circle.yml 32 | tslint.json 33 | npm-debug.log 34 | 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es6", 6 | "declaration": true, 7 | "strict": true, 8 | "sourceMap": true, 9 | "outDir": "lib", 10 | "moduleResolution": "node", 11 | "typeRoots": ["node_modules/@types"], 12 | "lib": ["es2015"], 13 | "types": ["node"] 14 | }, 15 | "formatCodeOptions": { 16 | "indentSize": 2, 17 | "tabSize": 2 18 | }, 19 | "exclude": [ 20 | "node_modules", 21 | "test", 22 | "lib" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "npm", 6 | "isShellCommand": true, 7 | "showOutput": "always", 8 | "suppressTaskName": true, 9 | "tasks": [ 10 | { 11 | "taskName": "build", 12 | "args": ["run", "build"] 13 | }, 14 | { 15 | "taskName": "install", 16 | "args": ["install"] 17 | }, 18 | { 19 | "taskName": "update", 20 | "args": ["update"] 21 | }, 22 | { 23 | "taskName": "test", 24 | "args": ["run", "test"] 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "member-access": false, 5 | "no-console": [ 6 | false 7 | ], 8 | "object-literal-sort-keys": false, 9 | "variable-name": [ 10 | true, 11 | "ban-keywords" 12 | ], 13 | "whitespace": [ 14 | true, 15 | "check-branch", 16 | "check-decl", 17 | "check-operator", 18 | "check-separator", 19 | "check-type" 20 | ], 21 | "no-var-requires": false, 22 | "no-require-imports": false, 23 | "interface-name": [ 24 | true, 25 | "never-prefix" 26 | ], 27 | "max-classes-per-file": false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Circle CI nightly builder", 3 | "description": "Heroku app to trigger Circle CI build to create maintenance pull requests", 4 | "keywords": [ 5 | "CircleCI", 6 | "GitHub" 7 | ], 8 | "env": { 9 | "PROJECT": { 10 | "description": "GitHub repository to build, e.g. gfx/ci-npm-update" 11 | }, 12 | "BRANCH": { 13 | "description": "Git branch to build", 14 | "value": "master" 15 | }, 16 | "CIRCLECI_TOKEN": { 17 | "description": "CircleCI: Project Settings -> API Permissions -> Create a token with 'All' scope" 18 | }, 19 | "TZ": { 20 | "description": "TimeZone for the app", 21 | "value": "Asia/Tokyo", 22 | "required": false 23 | }, 24 | "TRIGGER_NAME": { 25 | "value": "NPM_UPDATE" 26 | } 27 | }, 28 | "addons": [ 29 | "scheduler:standard" 30 | ] 31 | } -------------------------------------------------------------------------------- /test/compare_view.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | import { GitHubCompareView } from "../src/compare_view"; 3 | 4 | describe("CompareView", () => { 5 | context(".fixupUrl", () => { 6 | it("can fix https uri", () => { 7 | assert(GitHubCompareView.fixupUrl({ 8 | url: "https://github.com/bitjourney/ci-npm-update.git", 9 | }) === "https://github.com/bitjourney/ci-npm-update"); 10 | }); 11 | 12 | it("can fix git SSH uri", () => { 13 | assert(GitHubCompareView.fixupUrl({ 14 | url: "git@github.com:bitjourney/ci-npm-update.git", 15 | }) === "https://github.com/bitjourney/ci-npm-update"); 16 | }); 17 | 18 | it("can fix git+https uri", () => { 19 | assert(GitHubCompareView.fixupUrl({ 20 | url: "git+https://github.com/bitjourney/ci-npm-update", 21 | }) === "https://github.com/bitjourney/ci-npm-update"); 22 | }); 23 | 24 | it("returns null for garbage", () => { 25 | assert(GitHubCompareView.fixupUrl({ 26 | url: "this is not a repository url", 27 | }) === null); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # The Reivision History of ci-npm-update 2 | 3 | ## v2.0.0 - 2017/07/11 4 | 5 | * Support npm@5's `package-lock.json` 6 | * NOTE: `npm-shrinkwrap.json` is no longer supported, but you can easily migrate it to `package-lock.json` 7 | * See [The npm Blog — v5\.0\.0](http://blog.npmjs.org/post/161081169345/v500) for deails of npm@5 8 | * Fix scoped packages in pull-request descriptions, esp. `@types/*` 9 | 10 | 11 | ## v1.0.9 - 2016/10/23 12 | 13 | * Fix maxBuffer exceeded errors [#68](https://github.com/bitjourney/ci-npm-update/pull/68) 14 | 15 | ## v1.0.8 - 2016/09/30 16 | 17 | * Avoid crashes when repository URLs are not a valid git repository (#50, #51) 18 | 19 | ## v1.0.7 - 2016/09/12 20 | 21 | * Support git+ssh protocols (#37) 22 | 23 | ## v1.0.6 - 2016/08/27 24 | 25 | * Fix an issue that git protocol URLs caused crashes (#31) 26 | 27 | ## v1.0.5 - 2016/08/27 28 | 29 | * Fix issue contents 30 | 31 | ## v1.0.4 - 2016/08/19 32 | 33 | * Fix an issue that git protocol URLs caused crashes (#24) 34 | 35 | ## v1.0.3 - 2016/08/13 36 | 37 | * Fix issues that packages that have no repository url made broken links 38 | 39 | ## v1.0.2 - 2016/08/12 40 | 41 | * Avoid making a pull-request when there's no diff in dependencies 42 | * Fix repository URLs in issue bodies 43 | 44 | ## v1.0.1 - 2016/08/05 45 | 46 | * Fix version specs in dependencies 47 | 48 | ## v1.0.0 - 2016/07/31 49 | 50 | * Initial stable release. 51 | -------------------------------------------------------------------------------- /test/package_lock.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as assert from "power-assert"; 3 | 4 | import { PackageLock } from "../src/package_lock"; 5 | 6 | describe("PackageLock", () => { 7 | context(".read", () => { 8 | it("reads package-lock.json", async () => { 9 | const packageLock = await PackageLock.read(path.join(__dirname, "fixture/package-lock.json")); 10 | assert(packageLock.getDependencyNames().length === 1); 11 | assert(packageLock.getDependencyNames()[0] === "ts-node"); 12 | }); 13 | }); 14 | 15 | context(".diff", () => { 16 | it("calculate the diff with two package-lock.json", async () => { 17 | const packageLock1 = await PackageLock.read(path.join(__dirname, "fixture/package-lock.json")); 18 | const packageLock2 = await PackageLock.read(path.join(__dirname, "fixture/package-lock-updated.json")); 19 | 20 | const diffs = await packageLock1.diff(packageLock2); 21 | assert(diffs.length === 1); 22 | 23 | const diff = diffs[0]; 24 | assert(diff.name === "ts-node"); 25 | assert(diff.installedVersion === "1.7.3"); 26 | assert(diff.latestVersion === "3.1.0"); 27 | assert(diff.getRepositoryUrl() === "https://github.com/TypeStrong/ts-node"); 28 | assert(diff.getDiffUrl() === "https://github.com/TypeStrong/ts-node/compare/v1.7.3...v3.1.0"); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ci-npm-update", 3 | "version": "2.0.0", 4 | "description": "Keep NPM dependencies up-to-date with CI, providing version-to-version diff for each library", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:bitjourney/ci-npm-update.git" 8 | }, 9 | "main": "index.js", 10 | "bin": "bin/ci-npm-update", 11 | "scripts": { 12 | "dry-run": "npm run build && bin/ci-npm-update", 13 | "preversion": "npm test", 14 | "postpublish": "git push origin master && git push origin --tags", 15 | "setup": "npm install && npm dedupe", 16 | "build": "npm run lint && tsc", 17 | "lint": "tslint 'src/**/*.ts' 'test/**/*.ts'", 18 | "lint:fix": "tslint --fix 'src/**/*.ts' 'test/**/*.ts'", 19 | "test": "TS_NODE_PROJECT=test/tsconfig.json mocha --opts test/support/default.opts test/**/*.test.ts" 20 | }, 21 | "keywords": [ 22 | "npm", 23 | "update", 24 | "github", 25 | "ci" 26 | ], 27 | "author": "FUJI Goro", 28 | "license": "Apache-2.0", 29 | "engines": { 30 | "node": ">= 6.0.0", 31 | "npm": ">= 5.0.0" 32 | }, 33 | "dependencies": { 34 | "@types/node": "^6.0.41", 35 | "moment": "^2.14.1", 36 | "source-map-support": "^0.4.15", 37 | "request": "^2.74.0" 38 | }, 39 | "devDependencies": { 40 | "@types/mocha": "*", 41 | "@types/power-assert": "*", 42 | "espower-typescript": "^8.0.0", 43 | "mocha": "*", 44 | "power-assert": "*", 45 | "to-iso-string": "*", 46 | "ts-node": "*", 47 | "tslint": "^5.4.3", 48 | "typescript": "*" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/compare_view.ts: -------------------------------------------------------------------------------- 1 | import { GitHubApi } from "./github"; 2 | import { NpmConfig } from "./npm_config"; 3 | 4 | export class GitHubCompareView { 5 | 6 | static fixupUrl(repository: { url: string } | undefined): string | null { 7 | if (!(repository && repository.url)) { 8 | return null; 9 | } 10 | try { 11 | const data = GitHubApi.parseUrl(repository.url); 12 | return `https://${data.host}/${data.owner}/${data.repository}`; 13 | } catch (e) { 14 | return null; 15 | } 16 | } 17 | 18 | name: string; 19 | 20 | installedVersion: string | null; 21 | latestVersion: string | null; 22 | 23 | repositoryUrl: string | null; 24 | 25 | constructor(installedVersion: string | null, latestVersion: string | null, npmConfig: NpmConfig) { 26 | this.name = npmConfig.name || "(undefined name)"; 27 | this.installedVersion = installedVersion; 28 | this.latestVersion = latestVersion; 29 | this.repositoryUrl = GitHubCompareView.fixupUrl(npmConfig.repository); 30 | } 31 | 32 | hasRepositoryUrl(): boolean { 33 | return this.repositoryUrl ? true : false; 34 | } 35 | 36 | getRepositoryUrl(): string | null { 37 | return this.repositoryUrl; 38 | } 39 | 40 | hasDiffUrl(): boolean { 41 | return (this.installedVersion && this.latestVersion) ? true : false; 42 | } 43 | 44 | getVersionRange(): string { 45 | return `v${this.installedVersion}...v${this.latestVersion}`; 46 | } 47 | 48 | getDiffUrl(): string { 49 | return `${this.repositoryUrl}/compare/${this.getVersionRange()}`; 50 | } 51 | 52 | toPromise(): Promise { 53 | return Promise.resolve(this); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as main from "./index"; 3 | 4 | function die(message: string) { 5 | console.error(message); 6 | process.exit(1); 7 | } 8 | 9 | function help() { 10 | console.log(` 11 | ci-npm-update --token GITHUB_ACCESS_TOKEN --user-name NAME --user-email EMAIL --execute 12 | `.trim()); 13 | process.exit(); 14 | } 15 | 16 | function version() { 17 | console.log(`v${require("../package").version}`); 18 | process.exit(); 19 | } 20 | 21 | async function parseOptions() { 22 | const options: main.Options = { 23 | githubAccessToken: process.env.GITHUB_ACCESS_TOKEN, 24 | gitUserName: process.env.GIT_USER_NAME, 25 | gitUserEmail: process.env.GIT_USER_EMAIL, 26 | execute: false, 27 | }; 28 | 29 | const args = process.argv.splice(2); 30 | for (let i = 0; i < args.length; i++) { 31 | const arg = args[i]; 32 | if (arg === "--token") { 33 | if (++i === args.length) { 34 | die(`No value for ${arg}`); 35 | } 36 | options.githubAccessToken = args[i]; 37 | } else if (arg === "--user-name") { 38 | if (++i === args.length) { 39 | die(`No value for ${arg}`); 40 | } 41 | options.gitUserName = args[i]; 42 | } else if (arg === "--user-email") { 43 | if (++i === args.length) { 44 | die(`No value for ${arg}`); 45 | } 46 | options.gitUserEmail = args[i]; 47 | } else if (arg === "--execute") { 48 | options.execute = true; 49 | } else if (arg === "--list") { 50 | await main.listDependencies(); 51 | process.exit(); 52 | } else if (arg === "--help") { 53 | help(); 54 | } else if (arg === "--version") { 55 | version(); 56 | } else { 57 | die(`Unknown option: ${arg}`); 58 | } 59 | } 60 | return options; 61 | } 62 | 63 | parseOptions().then((options) => { 64 | return main.start(options); 65 | }).then((pullRequestUrl) => { 66 | console.log("Successfully creqted a pull-request: %s", pullRequestUrl); 67 | }).catch((reason) => { 68 | // handle expected reasons 69 | if (reason instanceof main.AllDependenciesAreUpToDate) { 70 | console.log("All the dependencies are up to date."); 71 | return; 72 | } else if (reason instanceof main.SkipToCreatePullRequest) { 73 | console.log("Skiped to create a pull-request because --execute is not specified."); 74 | return; 75 | } 76 | console.error(`Unexpected errors caught: ${reason.stack}`); 77 | process.exit(1); 78 | }); 79 | -------------------------------------------------------------------------------- /test/github.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | import { GitHubApi } from "../src/github"; 3 | import { NpmConfig } from "../src/npm_config"; 4 | 5 | const githubDataSet = { 6 | "github git url": "git://github.com/foo/something.useful.git", 7 | "github git+https url": "git+https://github.com/foo/something.useful.git", 8 | "github git+ssh url": "git+ssh://git@github.com/foo/something.useful.git", 9 | "github http url": "http://github.com/foo/something.useful", 10 | "github https url": "https://github.com/foo/something.useful", 11 | "github https with git extension url": "https://github.com/foo/something.useful.git", 12 | "github ssh url": "git@github.com:foo/something.useful.git", 13 | }; 14 | const gheDataSet = { 15 | "ghe ssh url": "git@ghe.example.com:foo/something.useful.git", 16 | "ghe https url": "https://ghe.example.com/foo/something.useful", 17 | "ghe https+git url": "https://ghe.example.com/foo/something.useful.git", 18 | }; 19 | describe("GitHubApi", () => { 20 | context(".parseUrl(reading github.com URLs)", () => { 21 | Object.keys(githubDataSet).forEach((name) => { 22 | const url = (githubDataSet as any)[name] as string; 23 | 24 | it(`extracts "endpoint" from ${name}`, () => { 25 | assert(GitHubApi.extractEndpoint(url) === "https://api.github.com"); 26 | }); 27 | 28 | it(`extracts "owner" from ${name}`, () => { 29 | assert(GitHubApi.extractOwner(url) === "foo"); 30 | }); 31 | 32 | it(`extracts "repository" from ${name}`, () => { 33 | assert(GitHubApi.extractRepository(url) === "something.useful"); 34 | }); 35 | }); 36 | }); 37 | 38 | context(".parseUrl(ghe URLs)", () => { 39 | Object.keys(gheDataSet).forEach((name) => { 40 | const url = (gheDataSet as any)[name] as string; 41 | 42 | it(`extracts "endpoint" from ${name}`, () => { 43 | assert(GitHubApi.extractEndpoint(url) === "https://ghe.example.com/api/v3"); 44 | }); 45 | 46 | it(`extracts "owner" from ${name}`, () => { 47 | assert(GitHubApi.extractOwner(url) === "foo"); 48 | }); 49 | 50 | it(`extracts "repository" from ${name}`, () => { 51 | assert(GitHubApi.extractRepository(url) === "something.useful"); 52 | }); 53 | }); 54 | }); 55 | context(".parseUrl(package.json)", () => { 56 | it(`can parse itself`, () => { 57 | const npmConfig = new NpmConfig(require("../package.json")); 58 | assert(GitHubApi.extractRepository(npmConfig.repository.url) === "ci-npm-update"); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/issue.ts: -------------------------------------------------------------------------------- 1 | // to file an issue from a package info list 2 | 3 | import { GitHubCompareView } from "./compare_view"; 4 | import { NpmConfig } from "./npm_config"; 5 | 6 | const SIGNATURE = "[bitjourney/ci-npm-update](https://github.com/bitjourney/ci-npm-update)"; 7 | 8 | export function createBody(list: GitHubCompareView[], npmConfigPromise: Promise): Promise { 9 | return npmConfigPromise.then((npmConfig) => { 10 | 11 | const map = new Map(); 12 | list.forEach((item) => { 13 | map.set(item.name, item); 14 | }); 15 | 16 | let s = ""; 17 | 18 | const dependencyNames = Object.keys(npmConfig.dependencies); 19 | if (dependencyNames.length !== 0) { 20 | const head = "\n## Dependencies declared in package.json\n\n"; 21 | let section = ""; 22 | 23 | dependencyNames.forEach((name) => { 24 | const compareView = map.get(name); 25 | if (compareView) { 26 | section += compareViewToMarkdown(compareView); 27 | map.delete(name); 28 | } 29 | }); 30 | 31 | if (section) { 32 | s += head + section; 33 | } 34 | } 35 | 36 | const devDependencyNames = Object.keys(npmConfig.devDependencies); 37 | if (devDependencyNames.length !== 0) { 38 | const head = "\n## DevDependencies declared in package.json\n\n"; 39 | let section = ""; 40 | 41 | devDependencyNames.forEach((name) => { 42 | const compareView = map.get(name); 43 | if (compareView) { 44 | section += compareViewToMarkdown(compareView); 45 | map.delete(name); 46 | } 47 | }); 48 | 49 | if (section) { 50 | s += head + section; 51 | } 52 | } 53 | 54 | if (map.size !== 0) { 55 | const head = "\n## Dependencies not declared in package.json\n\n"; 56 | let section = ""; 57 | 58 | list.forEach((c) => { 59 | const compareView = map.get(c.name); 60 | if (compareView) { 61 | section += compareViewToMarkdown(compareView); 62 | } 63 | }); 64 | 65 | if (section) { 66 | s += head + section; 67 | } 68 | } 69 | s += "\n\n"; 70 | s += "Powered by " + SIGNATURE; 71 | return s; 72 | }); 73 | } 74 | 75 | function compareViewToMarkdown(c: GitHubCompareView): string { 76 | if (c.hasRepositoryUrl()) { 77 | if (c.hasDiffUrl()) { 78 | return `* \`${c.name}\`: [${c.getVersionRange()}](${c.getDiffUrl()})\n`; 79 | } else { 80 | return `* \`${c.name}\` ${c.getRepositoryUrl()}\n`; 81 | } 82 | } else { 83 | return `* \`${c.name}\`\n`; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ci-npm-update [![CircleCI](https://circleci.com/gh/bitjourney/ci-npm-update.svg?style=svg)](https://circleci.com/gh/bitjourney/ci-npm-update) 2 | 3 | This command keeps npm dependencies up-to-date by making pull-requests from CI. 4 | 5 | For example: https://github.com/gfx/ci-npm-update/pull/13 6 | 7 | ![](doc-assets/ci-npm-update-pr.png) 8 | 9 | This is inspired by [circleci-bundle-update-pr](https://github.com/masutaka/circleci-bundle-update-pr). 10 | 11 | # Install 12 | 13 | ```console 14 | npm install --save-dev ci-npm-update 15 | ``` 16 | 17 | # Usage 18 | 19 | ## Configuration 20 | 21 | This command is designed to be executed by CI nightly builds. 22 | 23 | Set `GITHUB_ACCESS_TOKEN` environment to make a pull-request to github repositories, 24 | and set SSH keys to push to the repos from CI. 25 | 26 | If the CI environment has no git configuration, also set `GIT_USER_NAME` and `GIT_USER_EMAIL` 27 | to commit patches in CI. 28 | 29 | ```console 30 | export GITHUB_ACCESS_TOKEN=... 31 | export GIT_USER_NAME=gfx 32 | export GIT_USER_EMAIL=gfx@users.noreply.github.com 33 | ``` 34 | 35 | ## Execution 36 | 37 | By default, `ci-npm-update` runs in dry-run mode. Set `--execute` to make pull-requests. 38 | 39 | ```console 40 | ci-npm-update --execute 41 | ``` 42 | 43 | ## Local Tests 44 | 45 | If you only run it in dry-run mode, no configuration is required: 46 | 47 | ```console 48 | # run in dry-run mode: 49 | ci-npm-update 50 | ``` 51 | 52 | If you want to make pull-requests in your local machine, use `GITHUB_ACCESS_TOKEN`: 53 | 54 | ```console 55 | # envchain is recommended 56 | envchain --set github GITHUB_ACCESS_TOKEN 57 | # run: 58 | envchain github ci-npm-update --execute 59 | ``` 60 | 61 | In addition, `--list` is provided to list dependencies with short descriptions, intended for sanity check in development. 62 | 63 | ```console 64 | ci-npm-update --list 65 | ``` 66 | 67 | # Development 68 | 69 | Setup: 70 | 71 | ```console 72 | npm run setup 73 | ``` 74 | 75 | Easy test command in dry-run mode: 76 | 77 | ```console 78 | npm run build && envchain github node bin/ci-npm-update 79 | ``` 80 | 81 | ## Heroku Scheduler 82 | 83 | If you want to setup heroku schedulers, there's a template for it: 84 | 85 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/gfx/ci-npm-update) 86 | 87 | To test it, run the following command: 88 | 89 | ```console 90 | heroku run './build-circleci' 91 | ``` 92 | 93 | # License 94 | 95 | Copyright (c) 2016 Bit Journey, Inc. 96 | 97 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 98 | 99 | http://www.apache.org/licenses/LICENSE-2.0 100 | 101 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 102 | -------------------------------------------------------------------------------- /src/npm_config.ts: -------------------------------------------------------------------------------- 1 | const request = require("request"); 2 | import * as fs from "fs"; 3 | 4 | const REGISTRY_ENDPOINT = "https://registry.npmjs.org"; 5 | 6 | export interface DependencyMapType { 7 | readonly [name: string]: string; 8 | } 9 | 10 | export interface RepositoryType { 11 | readonly type: string; 12 | readonly url: string; 13 | } 14 | 15 | function escapePackageName(name: string) { 16 | return name.replace(/\//g, encodeURIComponent("/")); 17 | } 18 | 19 | export class NpmConfig { 20 | 21 | static readFromFile(packageJsonFile: string = "package.json"): Promise { 22 | return new Promise((resolve, reject) => { 23 | fs.readFile(packageJsonFile, "utf8", (err, data) => { 24 | if (err) { 25 | reject(err); 26 | } else { 27 | resolve(new NpmConfig(JSON.parse(data))); 28 | } 29 | }); 30 | }); 31 | } 32 | 33 | static getFromRegistry(name: string, version: string): Promise { 34 | return new Promise((resolve, _reject) => { 35 | const url = `${REGISTRY_ENDPOINT}/${escapePackageName(name)}/=${encodeURIComponent(version)}`; 36 | request(url, (err: any, res: any, body: any) => { 37 | if (err) { 38 | resolve(new NpmConfig({ 39 | name, 40 | version, 41 | error: err, 42 | })); 43 | return; 44 | } 45 | let json: any; 46 | try { 47 | json = JSON.parse(body); 48 | } catch (e) { 49 | const error = `Failed to get npm config from ${url}: ${res.statusCode} ${res.statusMessage}`; 50 | resolve(new NpmConfig({ 51 | name, 52 | version, 53 | error, 54 | })); 55 | return; 56 | } 57 | if (json.error) { 58 | const error = `Failed to get npm config from ${url}: ${json.error}`; 59 | resolve(new NpmConfig({ 60 | name, 61 | version, 62 | error, 63 | })); 64 | return; 65 | } 66 | resolve(new NpmConfig(json)); 67 | }); 68 | }); 69 | } 70 | 71 | // https://docs.npmjs.com/files/package.json 72 | 73 | readonly json: any; 74 | 75 | get name(): string { 76 | return this.json.name; 77 | } 78 | 79 | get version(): string { 80 | return this.json.version; 81 | } 82 | 83 | get description(): string | undefined | null { 84 | return this.json.description; 85 | } 86 | 87 | get repository(): RepositoryType { 88 | return this.json.repository; 89 | } 90 | 91 | dependencies: DependencyMapType; 92 | devDependencies: DependencyMapType; 93 | peerDependencies: DependencyMapType; 94 | 95 | constructor(json: any) { 96 | this.json = json; 97 | this.dependencies = (json.dependencies || {}) as DependencyMapType; 98 | this.devDependencies = (json.devDependencies || {}) as DependencyMapType; 99 | this.peerDependencies = (json.peerDependencies || {}) as DependencyMapType; 100 | } 101 | 102 | summary(): string { 103 | return `${this.name}@${this.version} - ${this.description || ""}`; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/package_lock.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { GitHubCompareView } from "./compare_view"; 3 | import { NpmConfig } from "./npm_config"; 4 | 5 | export const DEFAULT_LOCK_FILE = "package-lock.json"; 6 | 7 | export interface ShrinkWrapData { 8 | version: string; 9 | from: string; 10 | resolved: string; 11 | } 12 | 13 | export class PackageLock { 14 | 15 | static read(lockFile = DEFAULT_LOCK_FILE): Promise { 16 | return new Promise((resolve, reject) => { 17 | fs.readFile(lockFile, "utf8", (err, data) => { 18 | if (err) { 19 | reject(err); 20 | } else { 21 | resolve(new PackageLock(JSON.parse(data))); 22 | } 23 | }); 24 | }); 25 | } 26 | 27 | name: string; 28 | version: string; 29 | 30 | private dependencies: Map; 31 | 32 | constructor(json: { name: any, version: any, dependencies: any }) { 33 | this.name = json.name; 34 | this.version = json.version; 35 | this.dependencies = json.dependencies; 36 | } 37 | 38 | getDependencyNames(): string[] { 39 | return Object.keys(this.dependencies); 40 | } 41 | 42 | getDependencyData(name: string): ShrinkWrapData { 43 | return (this.dependencies as any)[name]; 44 | } 45 | 46 | getDependencyVersion(name: string): string { 47 | const version = this.getDependencyData(name).version; 48 | console.assert(version, `version of ${name}`); 49 | return version; 50 | } 51 | 52 | getDependencyVersionRange(name: string): string { 53 | const parts = this.getDependencyData(name).from.split(/@/); 54 | return parts[parts.length - 1]; 55 | } 56 | 57 | diff(other: PackageLock): Promise { 58 | return new ShrinkWrapDiff(this, other).getCompareViewList(); 59 | } 60 | } 61 | 62 | export class ShrinkWrapDiff { 63 | older: PackageLock; 64 | newer: PackageLock; 65 | 66 | constructor(older: PackageLock, newer: PackageLock) { 67 | this.older = older; 68 | this.newer = newer; 69 | } 70 | 71 | hasNoDiff(): boolean { 72 | return JSON.stringify(this.older) === JSON.stringify(this.newer); 73 | } 74 | 75 | getCompareViewList(): Promise { 76 | const older = this.older; 77 | const newer = this.newer; 78 | const union = new Set([ 79 | ...older.getDependencyNames(), 80 | ...newer.getDependencyNames(), 81 | ]); 82 | 83 | const result: Array> = []; 84 | union.forEach((name) => { 85 | const olderOne = older.getDependencyData(name); 86 | const newerOne = newer.getDependencyData(name); 87 | 88 | if (olderOne && !newerOne) { 89 | // removed 90 | return; 91 | } 92 | 93 | if (olderOne && newerOne && olderOne.version === newerOne.version) { 94 | // no change 95 | return; 96 | } 97 | result.push(NpmConfig.getFromRegistry(name, newerOne.version) 98 | .then((npmConfig) => { 99 | const olderVersion = olderOne ? olderOne.version : null; 100 | const newerVersion = newerOne ? newerOne.version : null; 101 | return new GitHubCompareView(olderVersion, newerVersion, npmConfig).toPromise(); 102 | })); 103 | }); 104 | return Promise.all(result); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/github.ts: -------------------------------------------------------------------------------- 1 | const request = require("request"); 2 | 3 | const USER_AGENT = "ci-npm-update/1.0"; 4 | 5 | export interface GitHubPullRequestResponse { 6 | id: string; 7 | url: string; 8 | html_url: string; 9 | } 10 | 11 | export interface GitHubPullRequestParameters { 12 | title: string; 13 | body: string; 14 | head: string; 15 | base: string; 16 | } 17 | 18 | export interface GitHubTagResponse { 19 | name: string; 20 | } 21 | 22 | export class GitHubApi { 23 | 24 | static parseUrl(url: string): { host: string, owner: string, repository: string } { 25 | // url can be: 26 | // git://github.com/foo/bar.git 27 | // git@github.com:foo/bar.git 28 | // git+https://github.com/foo/bar.git 29 | // git+ssh://git@github.com/foo/bar.git 30 | // http://github.com/foo/bar.baz.git 31 | // https://github.com/foo/bar.baz.git 32 | const schemeRegexps = [ 33 | "git:\\/\\/", 34 | "git@", 35 | "git\\+https:\\/\\/", 36 | "git\\+ssh:\\/\\/git@", 37 | "http:\\/\\/", 38 | "https:\\/\\/", 39 | "ssh://git@", 40 | ]; 41 | const hostRegexp = "([^\\/:]+)[\\/:]"; 42 | const pathRegexp = "([^\\/]+)\\/([^\\/]+)(?!\\.git)"; 43 | const matched = new RegExp(`^(?:${schemeRegexps.join("|")})${hostRegexp}${pathRegexp}$`).exec(url); 44 | if (!matched) { 45 | throw Error(`Cannot parse git repository URL: ${url}`); 46 | } 47 | return { 48 | host: matched[1], 49 | owner: matched[2], 50 | repository: matched[3].replace(/\.git$/, ""), 51 | }; 52 | } 53 | 54 | static extractEndpoint(url: string): string { 55 | const host = this.parseUrl(url).host; 56 | if (host === "github.com") { 57 | return "https://api.github.com"; 58 | } else { 59 | // https://developer.github.com/v3/enterprise/ 60 | return `https://${host}/api/v3`; 61 | } 62 | } 63 | static extractOwner(url: string): string { 64 | return this.parseUrl(url).owner; 65 | } 66 | static extractRepository(url: string): string { 67 | return this.parseUrl(url).repository; 68 | } 69 | 70 | endpoint: string; 71 | token: string; 72 | owner: string; 73 | repository: string; 74 | 75 | constructor(options: { 76 | repositoryUrl: string, 77 | token: string, 78 | }) { 79 | this.endpoint = GitHubApi.extractEndpoint(options.repositoryUrl); 80 | this.token = options.token; 81 | this.owner = GitHubApi.extractOwner(options.repositoryUrl); 82 | this.repository = GitHubApi.extractRepository(options.repositoryUrl); 83 | } 84 | 85 | buildRequestHeaders() { 86 | return { 87 | "user-agent": USER_AGENT, 88 | "authorization": `token ${this.token}`, 89 | }; 90 | } 91 | 92 | getTags(): Promise { 93 | return new Promise((resolve, reject) => { 94 | const url = `${this.endpoint}/repos/${this.owner}/${this.repository}/tags`; 95 | request.get(url, 96 | { 97 | headers: this.buildRequestHeaders(), 98 | }, 99 | (err: any, _response: any, body: any) => { 100 | if (err) { 101 | reject(err); 102 | } else if (body.errors) { 103 | reject(new Error(`Failed to create a pull request (${url}): ${JSON.stringify(body)}`)); 104 | } else { 105 | resolve(body); 106 | } 107 | }); 108 | }); 109 | } 110 | 111 | createPullRequest(parameters: GitHubPullRequestParameters): Promise { 112 | return new Promise((resolve, reject) => { 113 | // https://developer.github.com/v3/pulls/#create-a-pull-request 114 | const url = `${this.endpoint}/repos/${this.owner}/${this.repository}/pulls`; 115 | request.post(url, 116 | { 117 | headers: this.buildRequestHeaders(), 118 | json: { 119 | title: parameters.title, 120 | body: parameters.body, 121 | head: parameters.head, 122 | base: parameters.base, 123 | }, 124 | }, 125 | (err: any, _response: any, body: any) => { 126 | if (err) { 127 | reject(err); 128 | } else if (body.errors || !body.html_url) { 129 | reject(new Error(`Failed to create a pull request (${url}): ${JSON.stringify(body)}`)); 130 | } else { 131 | resolve(body); 132 | } 133 | }); 134 | }); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import * as moment from "moment"; 3 | import * as github from "./github"; 4 | import * as Issue from "./issue"; 5 | import { NpmConfig } from "./npm_config"; 6 | import { DEFAULT_LOCK_FILE, PackageLock } from "./package_lock"; 7 | 8 | function run(command: string): Promise { 9 | return new Promise((resolve, reject) => { 10 | console.log(`>> ${command}`); 11 | exec(command, { 12 | encoding: "utf8", 13 | maxBuffer: 1024 * 1024, 14 | }, (error, stdout, stderr) => { 15 | if (stdout.length > 0) { 16 | console.log(stdout); 17 | } 18 | if (stderr.length > 0) { 19 | console.error(stderr); 20 | } 21 | if (error) { 22 | reject(error); 23 | } else { 24 | resolve(stdout); 25 | } 26 | }); 27 | }); 28 | } 29 | 30 | export abstract class SkipRemainingTasks { } 31 | 32 | export class AllDependenciesAreUpToDate extends SkipRemainingTasks { } 33 | 34 | export class SkipToCreatePullRequest extends SkipRemainingTasks { } 35 | 36 | export interface Options { 37 | githubAccessToken: string; 38 | gitUserName: string; 39 | gitUserEmail: string; 40 | execute: boolean; // default to dry-run mode 41 | } 42 | 43 | export function setupGitConfig(gitUserName: string, gitUserEmail: string): Promise { 44 | const setUserNamePromise = gitUserName ? run(`git config user.name '${gitUserName}'`) : Promise.resolve(); 45 | const setUserEmailPromise = gitUserEmail ? run(`git config user.email '${gitUserEmail}'`) : Promise.resolve(); 46 | return Promise.all([setUserNamePromise, setUserEmailPromise]); 47 | } 48 | 49 | export async function createGitBranch(branch: string): Promise { 50 | console.log(`Creating a branch: ${branch}`); 51 | 52 | await run(`git checkout -b ${branch}`); 53 | await run(`rm -rf node_modules ${DEFAULT_LOCK_FILE}`); 54 | 55 | // npm update --depth 9999 might cause OOM: 56 | // https://github.com/npm/npm/issues/11876 57 | await run(`npm install`); 58 | 59 | await run(`git add ${DEFAULT_LOCK_FILE}`); 60 | const diff = (await run("git diff --cached")).trim(); 61 | 62 | if (diff) { 63 | await run(`git commit -m 'update npm dependencies'`); 64 | } else { 65 | await run("git checkout -"); 66 | return Promise.reject(new AllDependenciesAreUpToDate()); 67 | } 68 | 69 | const packageLock = await PackageLock.read(); 70 | await run("git checkout -"); 71 | return Promise.resolve(packageLock); 72 | } 73 | 74 | export async function start({ 75 | githubAccessToken: githubAccessToken, 76 | gitUserName: gitUserName, 77 | gitUserEmail: gitUserEmail, 78 | execute: execute, 79 | }: Options): Promise { 80 | if (execute) { 81 | console.assert(githubAccessToken, "Missing GITHUB_ACCESS_TOKEN or --token"); 82 | } 83 | 84 | const repositoryUrl = (await run("git remote get-url --push origin")).trim(); 85 | 86 | const githubApi = new github.GitHubApi({ 87 | repositoryUrl, 88 | token: githubAccessToken, 89 | }); 90 | 91 | const timestamp = moment().format("YYYYMMDDhhmmss"); 92 | const branch = `npm-update/${timestamp}`; 93 | 94 | await setupGitConfig(gitUserName, gitUserEmail); 95 | 96 | const packageLock = await PackageLock.read(); 97 | const updatedPackageLock = await createGitBranch(branch); 98 | const compareViewList = await packageLock.diff(updatedPackageLock); 99 | 100 | if (compareViewList.length === 0) { 101 | // There're only diffs in sub dependencies 102 | // e.g. https://github.com/bitjourney/ci-npm-update/pull/21/files 103 | return Promise.reject(new AllDependenciesAreUpToDate()); 104 | } 105 | 106 | const issue = await Issue.createBody(compareViewList, NpmConfig.readFromFile()); 107 | 108 | console.log("-------"); 109 | console.log(issue); 110 | console.log("--------"); 111 | 112 | if (execute) { 113 | await run(`git push origin ${branch}`); 114 | } else { 115 | console.log("Skipped `git push` because --execute is not specified."); 116 | } 117 | 118 | const baseBranch = (await run("git rev-parse --abbrev-ref HEAD")).trim(); 119 | 120 | if (execute) { 121 | const response = await githubApi.createPullRequest({ 122 | title: `npm update at ${new Date()}`, 123 | body: issue, 124 | head: branch, 125 | base: baseBranch.trim(), 126 | }); 127 | return Promise.resolve(response.html_url); 128 | } else { 129 | return Promise.reject(new SkipToCreatePullRequest()); 130 | } 131 | } 132 | 133 | export async function listDependencies() { 134 | const packageLock = await PackageLock.read(); 135 | for (const name of packageLock.getDependencyNames()) { 136 | const version = packageLock.getDependencyVersion(name); 137 | const npmConfig = await NpmConfig.getFromRegistry(name, version); 138 | console.log(npmConfig.summary()); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | 196 | --------------------------------------------------------------------------------