Skip to content

Commit

Permalink
doc: add comments on possible implementation of monorepo support
Browse files Browse the repository at this point in the history
  • Loading branch information
Realtin authored and hulkoba committed Mar 26, 2018
1 parent 5e7d933 commit cb26e18
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 0 deletions.
1 change: 1 addition & 0 deletions content/initial-pr.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const faqText = () => md`
There is a collection of [frequently asked questions](https://greenkeeper.io/faq.html). If those don’t help, you can always [ask the humans behind Greenkeeper](https://github.com/greenkeeperio/greenkeeper/issues/new).
`

// needs to handle files as an array of arrays!
function hasLockFileText (files) {
const lockFiles = _.pick(files, ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'])
const lockFile = _.findKey(lockFiles)
Expand Down
10 changes: 10 additions & 0 deletions jobs/create-group-version-branch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Incomming: dependency change, repositoryDoc

// get the groups and check if dependency is found in those package.jsons
// return if not or if group ignores the dependency

// sort the dependency change per package.json per type
// (prioritize 'dependency', filter out peerDependency.. ect see registry-change job)

// create branch for each group, wait for status, create PR .. ect (see create-version-branch)
// we need different commit messages for each dependency type
6 changes: 6 additions & 0 deletions jobs/create-initial-branch.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ module.exports = async function ({ repositoryId }) {
}

await updateRepoDoc(installationId, repoDoc)

// Object.keys(repoDoc.packages).length > 0
if (!_.get(repoDoc, ['packages', 'package.json'])) {
log.warn('exited: No packages and package.json found')
return
Expand All @@ -62,6 +64,8 @@ module.exports = async function ({ repositoryId }) {

const registry = RegClient()
const registryGet = promisify(registry.get.bind(registry))
// get for all package.jsons
// every package should be updated to the newest version
const dependencyMeta = _.flatten(
['dependencies', 'devDependencies', 'optionalDependencies'].map(type => {
return _.map(pkg[type], (version, name) => ({ name, version, type }))
Expand Down Expand Up @@ -152,6 +156,8 @@ module.exports = async function ({ repositoryId }) {
const privateBadgeRegex = new RegExp(`https://${env.BADGES_HOST}.+?.svg\\?token=\\w+(&ts=\\d+)?`)

let badgeAlreadyAdded = false
// create a transform loop for all the package.json paths and push into the transforms array below
// add .greenkeeperrc too!
const transforms = [
{
path: 'package.json',
Expand Down
217 changes: 217 additions & 0 deletions jobs/create-subgroup-initial-branch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
const Log = require('gk-log')
const _ = require('lodash')
const jsonInPlace = require('json-in-place')
const { promisify } = require('bluebird')
const semver = require('semver')
const RegClient = require('../lib/npm-registry-client')
const env = require('../lib/env')
const getRangedVersion = require('../lib/get-ranged-version')
const dbs = require('../lib/dbs')
const getConfig = require('../lib/get-config')
const createBranch = require('../lib/create-branch')
const { updateRepoDoc } = require('../lib/repository-docs')
const githubQueue = require('../lib/github-queue')
const { maybeUpdatePaymentsJob } = require('../lib/payments')
const upsert = require('../lib/upsert')

const registryUrl = env.NPM_REGISTRY

// if we update dependencies find open PRs for that dependency and close the PRs by commit message

module.exports = async function ({ repositoryId, groupname }) {
const { installations, repositories, logs } = await dbs()
const repoDoc = await repositories.get(repositoryId)
const accountId = repoDoc.accountId
const installation = await installations.get(accountId)
const installationId = installation.installation
const log = Log({logsDb: logs, accountId, repoSlug: repoDoc.fullName, context: 'create-initial-subgroup-branch'})

log.info('started')

if (repoDoc.fork && !repoDoc.hasIssues) { // we should allways check if issues are disabled and exit
log.warn('exited: Issues disabled on fork')
return
}

await updateRepoDoc(installationId, repoDoc)

// Object.keys(repoDoc.packages).length > 0
if (!_.get(repoDoc, ['packages', 'package.json'])) {
log.warn('exited: No packages and package.json found')
return
}
await upsert(repositories, repoDoc._id, repoDoc)

const config = getConfig(repoDoc)
if (config.disabled) {
log.warn('exited: Greenkeeper is disabled for this repo in package.json')
return
}
const pkg = _.get(repoDoc, ['packages', 'package.json']) // this is duplicated code (merge with L44)
if (!pkg) return

const [owner, repo] = repoDoc.fullName.split('/')

await createDefaultLabel({ installationId, owner, repo, name: config.label })

const registry = RegClient()
const registryGet = promisify(registry.get.bind(registry))
// get for all package.jsons in a group
// every package should be updated to the newest version
const dependencyMeta = _.flatten(
['dependencies', 'devDependencies', 'optionalDependencies'].map(type => {
return _.map(pkg[type], (version, name) => ({ name, version, type }))
})
)
log.info('dependencies found', {parsedDependencies: dependencyMeta, packageJson: pkg})
let dependencies = await Promise.mapSeries(dependencyMeta, async dep => {
try {
dep.data = await registryGet(registryUrl + dep.name.replace('/', '%2F'), {
})
return dep
} catch (err) {
log.error('npm: Could not get package data', {dependency: dep})
}
})
let dependencyActionsLog = {}
dependencies = _(dependencies)
.filter(Boolean)
.map(dependency => {
let latest = _.get(dependency, 'data.dist-tags.latest')
if (_.includes(config.ignore, dependency.name)) {
dependencyActionsLog[dependency.name] = 'ignored in config'
return
}
// neither version nor range, so it's something weird (git url)
// better not touch it
if (!semver.validRange(dependency.version)) {
dependencyActionsLog[dependency.name] = 'invalid range'
return
}
// new version is prerelease
const oldIsPrerelease = _.get(
semver.parse(dependency.version),
'prerelease.length'
) > 0
const prereleaseDiff = oldIsPrerelease &&
semver.diff(dependency.version, latest) === 'prerelease'
if (
!prereleaseDiff &&
_.get(semver.parse(latest), 'prerelease.length', 0) > 0
) {
const versions = _.keys(_.get(dependency, 'data.versions'))
latest = _.reduce(versions, function (current, next) {
const parsed = semver.parse(next)
if (!parsed) return current
if (_.get(parsed, 'prerelease.length', 0) > 0) return current
if (semver.gtr(next, current)) return next
return current
})
}
// no to need change anything :)
if (semver.satisfies(latest, dependency.version)) {
dependencyActionsLog[dependency.name] = 'satisfies semver'
return
}
// no downgrades
if (semver.ltr(latest, dependency.version)) {
dependencyActionsLog[dependency.name] = 'would be a downgrade'
return
}
dependency.newVersion = getRangedVersion(latest, dependency.version)
dependencyActionsLog[dependency.name] = `updated to ${dependency.newVersion}`
return dependency
})
.filter(Boolean)
.value()

log.info('parsed dependency actions', {dependencyActionsLog})

const ghRepo = await githubQueue(installationId).read(github => github.repos.get({ owner, repo })) // wrap in try/catch
log.info('github: repository info', {repositoryInfo: ghRepo})

const branch = ghRepo.default_branch

const newBranch = config.branchPrefix + 'initial' + `-${groupname}`

let badgeAlreadyAdded = false
// create a transform loop for all the package.json paths and push into the transforms array below
const transforms = [
{
path: 'package.json',
message: 'chore(package): update dependencies',
transform: oldPkg => {
const oldPkgParsed = JSON.parse(oldPkg)
const inplace = jsonInPlace(oldPkg)

dependencies.forEach(({ type, name, newVersion }) => {
if (!_.get(oldPkgParsed, [type, name])) return

inplace.set([type, name], newVersion)
})
return inplace.toString()
}
}
]

const sha = await createBranch({ // try/catch
installationId,
owner,
repo,
branch,
newBranch,
transforms
})

if (!sha) {
// When there are no changes and the badge already exists we can enable right away
if (badgeAlreadyAdded) {
await upsert(repositories, repoDoc._id, { enabled: true })
log.info('Repository silently enabled')
return maybeUpdatePaymentsJob(accountId, repoDoc.private)
} else {
log.error('Could not create initial branch')
throw new Error('Could not create initial branch')
}
}

const depsUpdated = transforms[0].created
const travisModified = false
const badgeAdded = false

await upsert(repositories, `${repositoryId}:branch:${sha}`, {
type: 'branch',
initial: true,
sha,
base: branch,
head: newBranch,
processed: false,
depsUpdated,
travisModified,
badgeAdded
})

log.success('success')

return {
delay: 30 * 60 * 1000,
data: {
name: 'initial-timeout-pr',
repositoryId,
accountId
}
}
}

async function createDefaultLabel ({ installationId, name, owner, repo }) {
if (name !== false) {
try {
await githubQueue(installationId).write(github => github.issues.createLabel({
owner,
repo,
name,
color: '00c775'
}))
} catch (e) {}
}
}
11 changes: 11 additions & 0 deletions jobs/github-event/push.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ module.exports = async function (data) {
const branchRef = `refs/heads/${repository.default_branch}`
if (!data.head_commit || data.ref !== branchRef) return

// add .greenkeeperrc
const relevantFiles = [
'package.json',
'package-lock.json',
'npm-shrinkwrap.json',
'yarn.lock'
]

// if .greenkeeperrc
// updated repoDoc with new .greenkeeperrc
// if a package.json is added/deleted(renamed/moved) in the .greenkeeperrc or the groupname is deleted/changed
// close all open prs for that groupname

if (!hasRelevantChanges(data.commits, relevantFiles)) return
const repositoryId = String(repository.id)

Expand All @@ -31,6 +37,9 @@ module.exports = async function (data) {
if (after === repodoc.headSha) return
repodoc.headSha = after

// get path of changed package json
// always put package.jsons in the repoDoc (new & old)
// if remove event: delete key of package.json
const oldPkg = _.get(repodoc, ['packages', 'package.json'])
await updateRepoDoc(installation.id, repodoc)
const pkg = _.get(repodoc, ['packages', 'package.json'])
Expand Down Expand Up @@ -95,6 +104,8 @@ function updateDoc (repositories, repository, repodoc) {
)
}

// check for relevant files in all folders!
// currently we might just detect those files in the root directory
function hasRelevantChanges (commits, files) {
return _.some(files, file => {
return _.some(['added', 'removed', 'modified'], changeType => {
Expand Down
42 changes: 42 additions & 0 deletions jobs/registry-change.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,42 @@ module.exports = async function (
// we want to handle different distTags in the future
if (distTag !== 'latest') return

/*
Update: 'by_dependency' already handles multiple package.json files, but not in the same result.
You get one result per matching dependency per depencyType per file in `packages`. The `value`
object for each result (used below, in `filteredSortedPackages` for example), looks like:
"value": {
"fullName": "aveferrum/angular-material-demo",
"accountId": "462667",
"filename": "frontend/package.json", // <- yay, works
"type": "dependencies",
"oldVersion": "^4.2.4"
}
Then in a separate result, you’d get
"value": {
"fullName": "aveferrum/angular-material-demo",
"accountId": "462667",
"filename": "backend/package.json",
"type": "dependencies",
"oldVersion": "^4.2.4"
}
So we’d need to either completely change how that view works (boo), or maybe add a clever reduce (?),
or collect the results per repo in this file, so we only fire off 'create-version-branch' once per
repo, not once per file per repo.
Note that we also have these views that still need to be checked:
- pr_open_by_dependency
- branch_by_dependency
- issue_open_by_dependency
*/

// packages are a list of all repoDocs that have that dependency (should rename that)
const packages = (await repositories.query('by_dependency', {
key: dependency
})).rows
Expand All @@ -62,9 +98,15 @@ module.exports = async function (
'_id'
)

// check if package has a greenkeeperrc / more then 1 package json or package.json is in subdirectory
// continue with the rest but send all otheres to a 'new' version branch job

// Prioritize `dependencies` over all other dependency types
// https://github.com/greenkeeperio/greenkeeper/issues/409

// put all this logic in an utils function and return an object that we would need to start
// the version branch or group version branch job

const order = {
'dependencies': 1,
'devDependencies': 2,
Expand Down
2 changes: 2 additions & 0 deletions lib/create-branch.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ module.exports = async (

const ghqueue = githubQueue(installationId)

// needs to be able to get multiple messages since we might update
// dependencies and devDependencies in one branch
const commits = (await Promise.mapSeries(transforms, async (
{ path, transform, message, create },
index
Expand Down
Loading

0 comments on commit cb26e18

Please sign in to comment.