Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monorepos #140

Merged
merged 28 commits into from
Jun 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bef430a
docs: add plan
janl Apr 11, 2018
372f718
feat: make update.js testable, add first integration tests
janl Apr 11, 2018
7a1d6c4
deps: update lockfile
janl Apr 11, 2018
a8450d0
test(monorepo): test many monorepo permutations
janl Apr 11, 2018
f32d33c
feat: handle monorepos
janl Apr 11, 2018
b348b6c
fix: sub package naming
janl Apr 12, 2018
c9ff90b
fix: mocking travis for tests to be run on travis was to recursive fo…
janl Apr 12, 2018
7fcb646
fix: path resolution fixes
janl Apr 12, 2018
a0c2e41
more test path shnanigans, thanks git for not handling empty directories
janl Apr 12, 2018
d8a2419
fix: node 4 compat
janl Apr 12, 2018
36d33ca
fix: how has this ever worked
janl Apr 12, 2018
f10dcc0
feat: instead of detecting firstPush, we now look for a gk-lockfile c…
janl Apr 13, 2018
31c3b7a
feat: ignore node_modules when finding package.jsons
janl Apr 16, 2018
faf2a82
chore: debug
janl Apr 16, 2018
56fae7a
feat: detect dep updates in grou branches
janl Apr 17, 2018
4a9ab71
fix: reset to update branch
janl Apr 17, 2018
51592b1
fix: fix monorepo support (#155)
May 29, 2018
8080d9d
feat: add support for monorepo dependencies
Realtin May 30, 2018
a1146b2
fix: don't revert commits
Realtin Jun 12, 2018
bf0b071
chore: add greenkeeper-monorepo-definitions
Realtin Jun 12, 2018
23e186d
refactor: extractDependency returns an array
Realtin Jun 12, 2018
32714bb
feat: default branch is configurable
Realtin Jun 12, 2018
dcad0b4
doc: add documentation for all possible environment variables
Realtin Jun 12, 2018
3273138
fix: bail on greenkeeper initial branches
Realtin Jun 12, 2018
92c455d
chore: add publishConfig with `next` tag
Realtin Jun 13, 2018
dccc61b
fix: always retrun an empty array for mismatches
Realtin Jun 14, 2018
397b9c6
test: adapt to monorepo changes
Realtin Jun 14, 2018
d9aa066
doc: update README.md
Realtin Jun 14, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# greenkeeper-lockfile
# Greenkeeper Lockfile

After [enabling Greenkeeper for your repository](https://github.com/integration/greenkeeper) you can use this package to make it work with lockfiles, such as `npm-shrinkwrap.json`, `package-lock.json` or `yarn.lock`.

Expand All @@ -11,6 +11,21 @@ After [enabling Greenkeeper for your repository](https://github.com/integration/
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)

- [Greenkeeper Lockfile](#greenkeeper-lockfile)
- [Package Managers](#package-managers)
- [CI Services](#ci-services)
- [How does it work](#how-does-it-work)
- [Setup](#setup)
- [Using Greenkeeper with Monorepos](#using-greenkeeper-with-monorepos)
- [Testing multiple node versions](#testing-multiple-node-versions)
- [CircleCI workflows](#circleci-workflows)
- [TeamCity Setup](#teamcity-setup)
- [Configuration options](#configuration-options)
- [Contributing a CI Service](#contributing-a-ci-service)
- [Environment information](#environment-information)
- [Detecting your service](#detecting-your-service)
- [Testing your service](#testing-your-service)

## Package Managers

* ✅ npm _(including npm5)_
Expand Down Expand Up @@ -46,6 +61,10 @@ After [enabling Greenkeeper for your repository](https://github.com/integration/

3. Configure your CI to run `greenkeeper-lockfile-update` right before it executes your tests and `greenkeeper-lockfile-upload` right after it executed your tests.

_The next Step is only applicable greenkeeper-lockfile version 2 (with monorepo support)_

4. If you use a default branch that is **not** `master` then you have to add the environment variable `GK_LOCK_DEFAULT_BRANCH` with the name of your default branch to your CI.


### Example Travis CI configurations

Expand Down Expand Up @@ -80,6 +99,12 @@ after_script: greenkeeper-lockfile-upload

To run the lockfile-update script with custom command line arguments, set the `GK_LOCK_YARN_OPTS` environment variable to your needs (set it to `--ignore-engines`, for example). They will be appended to the `yarn add` command.

## Using Greenkeeper with Monorepos

greenkeeper-lockfile 2.0.0 offers support for monorepos. To use it make sure you install `greenkeeper-lockfile@2` explicitly.

If you are using a default branch on Github that is **not** called `master`, please set an Environment Variable `GK_LOCK_DEFAULT_BRANCH` with the name of your default branch in your CI.

## Testing multiple node versions

It is common to test multiple node versions and therefor have multiple test jobs for one build. In this case the lockfile will automatically be updated for every job, but only uploaded for the first one.
Expand All @@ -96,7 +121,7 @@ before_script: greenkeeper-lockfile-update
after_script: greenkeeper-lockfile-upload
```

### CircleCI workflows
## CircleCI workflows

In order to use `greenkeeper-lockfile` with CircleCI workflows, it must be in the first job run. Use [sequential job execution](https://circleci.com/docs/2.0/workflows/#sequential-job-execution-example) to ensure the job that runs `greenkeeper-lockfile` is always executed first. For example, if `greenkeeper-lockfile` is run in the `lockfile` job, all other jobs in the workflow must require the `lockfile` job to finish before running:

Expand All @@ -111,14 +136,23 @@ workflows:
- lockfile
```

### TeamCity Setup
## TeamCity Setup

In order for this to work with TeamCity, the build configuration needs to set
the following environment variables:

- VCS_ROOT_URL from the vcsroot.<vcsrootid>.url parameter
- VCS_ROOT_BRANCH from the teamcity.build.branch parameter

## Configuration options

| Environment Variable | default value | what is it for? |
| ------------- | ------------- | ------------- |
| GK_LOCK_YARN_OPTS | '' | Add yarn options that greenkeeper should use e.g. `--ignore-engines` |
| GK_LOCK_DEFAULT_BRANCH | 'master' | Set your default github branch name |
| GK_LOCK_COMMIT_AMEND | false | Lockfile commit should be amended to the regular Greenkeeper commit |
| GK_LOCK_COMMIT_NAME | 'greenkeeperio-bot' | Set your prefered git commit name |
| GK_LOCK_COMMIT_EMAIL | '[email protected]' | Set your prefered git commit email |

## Contributing a CI Service

Expand Down
5 changes: 0 additions & 5 deletions ci-services/bitrise.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const gitHelpers = require('../lib/git-helpers')

const env = process.env

// http://devcenter.bitrise.io/faq/available-environment-variables/
Expand All @@ -19,9 +17,6 @@ module.exports = {
repoSlug: parseRepoSlug(env.GIT_REPOSITORY_URL),
// The name of the current branch
branchName: env.BITRISE_GIT_BRANCH,
// Is this the first push on this branch
// i.e. the Greenkeeper commit
firstPush: gitHelpers.getNumberOfCommitsOnBranch(env.BITRISE_GIT_BRANCH) === 1,
// Is this a regular build
correctBuild: env.PR === 'false',
// Should the lockfile be uploaded from this build
Expand Down
1 change: 0 additions & 1 deletion ci-services/buildkite.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const env = process.env
module.exports = {
repoSlug: gitHelpers.getRepoSlug(env.BUILDKITE_REPO),
branchName: env.BUILDKITE_BRANCH,
firstPush: gitHelpers.getNumberOfCommitsOnBranch(env.BUILDKITE_BRANCH) === 1,
correctBuild: env.BUILDKITE_PULL_REQUEST === 'false',
uploadBuild: true
}
1 change: 0 additions & 1 deletion ci-services/circleci.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const env = process.env
module.exports = {
repoSlug: `${env.CIRCLE_PROJECT_USERNAME}/${env.CIRCLE_PROJECT_REPONAME}`,
branchName: env.CIRCLE_BRANCH,
firstPush: !env.CIRCLE_PREVIOUS_BUILD_NUM,
correctBuild: _.isEmpty(env.CI_PULL_REQUEST),
uploadBuild: env.CIRCLE_NODE_INDEX === `${env.BUILD_LEADER_ID || 0}`
}
3 changes: 0 additions & 3 deletions ci-services/codeship.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ module.exports = {
repoSlug: getRepoSlug(),
// The name of the current branch
branchName: env.CI_BRANCH,
// Is this the first push on this branch
// i.e. the Greenkeeper commit
firstPush: shouldUpdate(),
// Is this a regular build (use tag: ^greenkeeper/)
correctBuild: shouldUpdate(),
// Should the lockfile be uploaded from this build (use tag: ^greenkeeper/)
Expand Down
3 changes: 0 additions & 3 deletions ci-services/jenkins.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

const env = process.env

const gitHelpers = require('../lib/git-helpers')

// Jenkins reports the branch name and Git URL in a couple of different places depending on use of the new
// pipeline vs. older job types.
const gitUrl = env.CHANGE_URL || env.GIT_URL
Expand All @@ -16,7 +14,6 @@ const branchName = matchesGreenkeeper ? matchesGreenkeeper[0] : origBranch
module.exports = {
gitUrl,
branchName,
firstPush: env.BUILD_NUMBER === '1' || gitHelpers.getNumberOfCommitsOnBranch(env.GIT_BRANCH) === 1,
correctBuild: true, // assuming this is always the correct build to update the lockfile
uploadBuild: true // assuming 1 build per branch/PR
}
1 change: 0 additions & 1 deletion ci-services/semaphoreci.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const env = process.env
module.exports = {
repoSlug: env.SEMAPHORE_REPO_SLUG,
branchName: env.BRANCH_NAME,
firstPush: env.SEMAPHORE_BUILD_NUMBER === '1',
correctBuild: _.isEmpty(env.PULL_REQUEST_NUMBER),
uploadBuild: env.SEMAPHORE_CURRENT_JOB === '1'
}
1 change: 0 additions & 1 deletion ci-services/teamcity.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ function shouldUpload () {
module.exports = {
repoSlug: gitHelpers.getRepoSlug(env.VCS_ROOT_URL),
branchName: env.VCS_ROOT_BRANCH,
firstPush: shouldUpload(),
correctBuild: !isPullRequest(),
uploadBuild: shouldUpload()
}
3 changes: 0 additions & 3 deletions ci-services/travis.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ module.exports = {
repoSlug: env.TRAVIS_REPO_SLUG,
// The name of the current branch
branchName: env.TRAVIS_BRANCH,
// Is this the first push on this branch
// i.e. the Greenkeeper commit
firstPush: !env.TRAVIS_COMMIT_RANGE,
// Is this a regular build
correctBuild: env.TRAVIS_PULL_REQUEST === 'false',
// Should the lockfile be uploaded from this build
Expand Down
3 changes: 0 additions & 3 deletions ci-services/wercker.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
'use strict'

const gitHelpers = require('../lib/git-helpers')

const env = process.env

module.exports = {
repoSlug: `${env.WERCKER_GIT_OWNER}/${env.WERCKER_GIT_REPOSITORY}`,
branchName: env.WERCKER_GIT_BRANCH,
firstPush: gitHelpers.getNumberOfCommitsOnBranch(env.WERCKER_GIT_BRANCH) === 1,
correctBuild: env.WERCKER_GIT_DOMAIN === 'github.com',

// In wercker, only add the upload step to the pipeline you'd want to upload from
Expand Down
31 changes: 29 additions & 2 deletions lib/extract-dependency.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,34 @@ module.exports = function extractDependency (pkg, branchPrefix, branch) {
})
}))

return _.find(allDependencies, (dependency) => {
return branch === branchPrefix + dependency.name + '-' + dependency.version
// nicked from https://github.com/greenkeeperio/greenkeeper/blob/master/lib/validate-greenkeeper-json.js#L9
const groupRex = '([a-zA-Z0-9_-]+/)?'

const monorepoReleaseRex = RegExp(`${branchPrefix}${groupRex}monorepo.([a-zA-Z0-9_-]+)-([.0-9-]+)`)

if (monorepoReleaseRex.test(branch)) {
const monorepoDefinintions = require('greenkeeper-monorepo-definitions')
const monorepoDefinitionGroupName = branch.match(monorepoReleaseRex)[2]

if (!monorepoDefinitionGroupName) {
return console.error('Could not extract the dependency group name from the branch name.')
}

const monorepo = monorepoDefinintions[monorepoDefinitionGroupName]
if (!monorepo) {
return console.error(`${monorepoDefinitionGroupName} is missing in Greenkeeper's monorepo definitions`)
}

const dependencies = allDependencies.filter(dependency => {
return monorepo.includes(dependency.name)
})

return _.compact(dependencies)
}

const dependency = _.find(allDependencies, (dependency) => {
const rex = RegExp(`${branchPrefix}${groupRex}${dependency.name}-${dependency.version}`)
return rex.test(branch)
})
return _.compact([dependency])
}
37 changes: 37 additions & 0 deletions lib/git-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,42 @@ module.exports = {
return (
`${parsed[1]}/${parsed[2]}`
)
},
hasLockfileCommit: function hasLockfileCommit (info) {
const defaultBranch = process.env.GK_LOCK_DEFAULT_BRANCH || 'master'
// CI clones are often shallow clones. Let’s make sure we have enough to work with
// https://stackoverflow.com/a/44036486/242298
// console.log(`git config --replace-all remote.origin.fetch +refs/heads/*:refs/remotes/origin/*`)
exec(`git config --replace-all remote.origin.fetch +refs/heads/*:refs/remotes/origin/*`)
// console.log(`git fetch`)
exec(`git fetch`)
// console.log(`git checkout master`)
// Sometimes weird things happen with Git, so let's make sure that we have a clean
// working tree before we go in!
exec(`git stash`)
exec(`git checkout ${defaultBranch}`)

const reset = () => exec(`git checkout -`)

try {
exec(`git log --oneline origin/${info.branchName}...${defaultBranch} | grep 'chore(package): update lockfile'`)
} catch (e) {
if (e.status === 1 &&
e.stdout.toString() === '' &&
e.stderr.toString() === '') { // grep didn’t find anything, and we are fine with that
reset()
return false // no commits
} else if (e.status > 1) { // git or grep errored
reset()
throw e
} else {
// git succeeded, grep failed to match anything, but no error occured, e.g.: no commit yet
reset()
return false
}
}
// git succeeded, grep found a match, we have a commit already
reset()
return true
}
}
10 changes: 10 additions & 0 deletions lib/ignores.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const fs = require('fs')
module.exports = function ignores () {
const ignoreFile = '.gitignore'
if (!fs.existsSync(ignoreFile)) {
return []
}

const ignoreContents = fs.readFileSync(ignoreFile).toString()
return (ignoreContents.length && ignoreContents.split('\n')) || []
}
29 changes: 18 additions & 11 deletions lib/update-lockfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,10 @@ const yarnFlags = {
'optionalDependencies': ' -O'
}

module.exports = function updateLockfile (dependency, options) {
module.exports.updateLockfile = function updateLockfile (dependency, options) {
if (!options.yarn && semver.lt(exec('npm --version').toString().trim(), '3.0.0')) {
exec('npm shrinkwrap')
} else {
// revert and unstage the changes done by greenkeeper
exec('git revert -n HEAD')
exec('git reset HEAD')

if (options.yarn) {
const flag = yarnFlags[dependency.type]
const envArgs = process.env.GK_LOCK_YARN_OPTS ? ` ${process.env.GK_LOCK_YARN_OPTS.trim()}` : ''
Expand All @@ -40,29 +36,40 @@ module.exports = function updateLockfile (dependency, options) {
exec('npm5 -v')
npmBin = 'npm5'
} catch (err) {}

exec(`${npmBin} install${args}`)
}
}
}

const commitEmail = process.env.GK_LOCK_COMMIT_EMAIL ? process.env.GK_LOCK_COMMIT_EMAIL.trim() : '[email protected]'
const commitName = process.env.GK_LOCK_COMMIT_NAME ? process.env.GK_LOCK_COMMIT_NAME.trim() : 'greenkeeperio-bot'
const shouldAmend = !_.includes([undefined, `0`, 'false', 'null', 'undefined'], process.env.GK_LOCK_COMMIT_AMEND)

module.exports.stageLockfile = function stageLockfile () {
// make sure that we have changes to add
if (exec('git status --porcelain').toString() === '') return

// stage the updated lockfile
exec('git add npm-shrinkwrap.json 2>/dev/null || true')
exec('git add package-lock.json 2>/dev/null || true')
exec('git add yarn.lock 2>/dev/null || true')
}

module.exports.commitLockfiles = function commitLockfiles () {
const commitEmail = process.env.GK_LOCK_COMMIT_EMAIL ? process.env.GK_LOCK_COMMIT_EMAIL.trim() : '[email protected]'
const commitName = process.env.GK_LOCK_COMMIT_NAME ? process.env.GK_LOCK_COMMIT_NAME.trim() : 'greenkeeperio-bot'
const shouldAmend = !_.includes([undefined, `0`, 'false', 'null', 'undefined'], process.env.GK_LOCK_COMMIT_AMEND)

exec(`git config user.email "${commitEmail}"`)
exec(`git config user.name "${commitName}"`)

if (shouldAmend) {
exec(`git commit --amend --author="${commitName} <${commitEmail}>" --no-edit`)
} else {
const updateMessage = 'chore(package): update lockfile\n\nhttps://npm.im/greenkeeper-lockfile'
let lockfileWording
// either say "lockfile" or "lockfiles" depending on how many files are changed
if (exec('git status --porcelain').toString().split('\n').length > 2) {
lockfileWording = 'lockfiles'
} else {
lockfileWording = 'lockfile'
}
const updateMessage = `chore(package): update ${lockfileWording}\n\nhttps://npm.im/greenkeeper-lockfile`
exec(`git commit -m "${updateMessage}"`)
}
}
Loading