Skip to content
Snippets Groups Projects
Commit aebf03a4 authored by Rhys Arkins's avatar Rhys Arkins Committed by GitHub
Browse files

Major refactor to add npm tests (#24)

parent 122c1674
No related branches found
No related tags found
No related merge requests found
......@@ -2,3 +2,4 @@
/config.js
/npm-debug.log
/coverage
.DS_Store
......@@ -4,7 +4,7 @@ const config = {};
let logger = null;
module.exports = {
setLogger,
init,
initRepo,
// Package File
getPackageFile,
......@@ -19,13 +19,13 @@ module.exports = {
updatePr,
};
function setLogger(l) {
function init(token, l) {
config.token = token;
logger = l;
}
// Initialize GitHub by getting base branch and SHA
function initRepo(token, repoName) {
config.token = token;
function initRepo(repoName) {
config.repoName = repoName;
return getRepo()
......@@ -67,6 +67,7 @@ function getPackageFile(branchName) {
}
function getPackageFileContents(packageFile) {
logger.debug(`Retrieving ${config.repoName} ${packageFile}`);
config.packageFile = packageFile;
return getFileContents(config.packageFile);
}
......
......@@ -6,102 +6,108 @@ let logger = null;
module.exports = {
setLogger,
getAllDependencyUpgrades,
extractDependencies,
findUpgrades,
isRange,
isValidVersion,
};
function setLogger(l) {
logger = l;
}
function getAllDependencyUpgrades(packageContents) {
const allDependencyChecks = [];
const allDependencyUpgrades = [];
const dependencyTypes = ['dependencies', 'devDependencies'];
dependencyTypes.forEach((depType) => {
if (!packageContents[depType]) {
return;
}
Object.keys(packageContents[depType]).forEach((depName) => {
const currentVersion = packageContents[depType][depName];
if (!isValidVersion(currentVersion)) {
logger.verbose(`${depName}: Skipping invalid version ${currentVersion}`);
return;
}
allDependencyChecks.push(
getDependencyUpgrades(depName, currentVersion).then((res) => {
if (res.length > 0) {
logger.verbose(`${depName}: Upgrades = ${JSON.stringify(res)}`);
res.forEach((upgrade) => {
allDependencyUpgrades.push({
// Returns an array of current dependencies
function extractDependencies(packageJson) {
// loop through dependency types
const depTypes = ['dependencies', 'devDependencies'];
return depTypes.reduce((allDeps, depType) => {
// loop through each dependency within a type
const depNames = Object.keys(packageJson[depType]) || [];
return allDeps.concat(depNames.map(depName => ({
depType,
depName,
currentVersion,
upgradeType: upgrade.type,
newVersion: upgrade.version,
newVersionMajor: semver.major(upgrade.version),
workingVersion: upgrade.workingVersion,
});
currentVersion: packageJson[depType][depName],
})));
}, []);
}
function findUpgrades(dependencies) {
const allDependencyUpgrades = [];
// We create an array of promises so that they can be executed in parallel
return Promise.all(dependencies.reduce((promises, dep) => promises.concat(
getVersions(dep.depName)
.then(versions => getUpgrades(dep.depName, dep.currentVersion, versions))
.then((upgrades) => {
if (upgrades.length > 0) {
logger.verbose(`${dep.depName}: Upgrades = ${JSON.stringify(upgrades)}`);
upgrades.forEach((upgrade) => {
allDependencyUpgrades.push(Object.assign(dep, upgrade));
});
} else {
logger.verbose(`${depName}: No upgrades required`);
logger.verbose(`${dep.depName}: No upgrades required`);
}
return Promise.resolve();
}));
});
});
return Promise.all(allDependencyChecks).then(() => allDependencyUpgrades);
})
.catch((error) => {
logger.error(`Error finding upgrades for ${dep.depName}: ${error}`);
})), []))
// Return the upgrade array once all Promises are complete
.then(() => allDependencyUpgrades);
}
function getDependency(depName) {
function getVersions(depName) {
// supports scoped packages, e.g. @user/package
return got(`https://registry.npmjs.org/${depName.replace('/', '%2F')}`, {
json: true,
});
}).then(res => res.body.versions);
}
function getDependencyUpgrades(depName, currentVersion) {
return getDependency(depName).then((res) => {
if (!res.body.versions) {
logger.error(`${depName} versions is null`);
function getUpgrades(depName, currentVersion, versions) {
if (!isValidVersion(currentVersion)) {
logger.verbose(`${depName} currentVersion is invalid`);
return [];
}
if (!versions) {
logger.verbose(`${depName} versions is null`);
return [];
}
const allUpgrades = {};
let workingVersion = currentVersion;
// Check for a current range and pin it
if (isRange(currentVersion)) {
// Pin ranges to their maximum satisfying version
const maxSatisfying = semver.maxSatisfying(
Object.keys(res.body.versions),
currentVersion);
allUpgrades.pin = { type: 'pin', version: maxSatisfying };
const maxSatisfying = semver.maxSatisfying(Object.keys(versions), currentVersion);
allUpgrades.pin = { upgradeType: 'pin', newVersion: maxSatisfying };
workingVersion = maxSatisfying;
}
const currentMajor = semver.major(workingVersion);
Object.keys(res.body.versions).forEach((version) => {
if (stable.is(workingVersion) && !stable.is(version)) {
// Loop through all possible versions
Object.keys(versions).forEach((newVersion) => {
if (stable.is(workingVersion) && !stable.is(newVersion)) {
// Ignore unstable versions, unless the current version is unstable
return;
}
if (semver.gt(version, workingVersion)) {
if (semver.gt(newVersion, workingVersion)) {
// Group by major versions
const thisMajor = semver.major(version);
if (
!allUpgrades[thisMajor] ||
semver.gt(version, allUpgrades[thisMajor].version)
) {
allUpgrades[thisMajor] = {
type: thisMajor > currentMajor ? 'major' : 'minor',
version,
const newVersionMajor = semver.major(newVersion);
// Save this, if it's a new major version or greater than the previous greatest
if (!allUpgrades[newVersionMajor] ||
semver.gt(newVersion, allUpgrades[newVersionMajor].newVersion)) {
const upgradeType = newVersionMajor > semver.major(workingVersion) ? 'major' : 'minor';
allUpgrades[newVersionMajor] = {
upgradeType,
newVersion,
newVersionMajor,
workingVersion,
};
}
}
});
if (allUpgrades.pin && Object.keys(allUpgrades).length > 1) {
// Remove the pin
// Remove the pin if we found upgrades
delete allUpgrades.pin;
}
// Return only the values
// Return only the values - we don't need the keys anymore
return Object.keys(allUpgrades).map(key => allUpgrades[key]);
});
}
function isRange(input) {
......
......@@ -11,7 +11,7 @@ module.exports = function init(setConfig) {
logger = config.logger;
// Initialize helpers
github.setLogger(logger);
github.init(config.token, logger);
npm.setLogger(logger);
packageJson.setLogger(logger);
changelog.setGitHubToken(config.token);
......@@ -21,12 +21,10 @@ module.exports = function init(setConfig) {
// This function manages the queue per-package file
function processPackageFile(repoName, packageFile) {
return github.initRepo(config.token, repoName)
.then(() => {
logger.info(`Processing ${repoName} ${packageFile}`);
return github.getPackageFileContents(packageFile);
})
.then(npm.getAllDependencyUpgrades)
return github.initRepo(repoName)
.then(() => github.getPackageFileContents(packageFile))
.then(npm.extractDependencies)
.then(npm.findUpgrades)
.then(processUpgradesSequentially)
.then(() => { // eslint-disable-line promise/always-return
logger.info(`${repoName} ${packageFile} done`);
......@@ -148,6 +146,10 @@ function updateDependency(upgrade) {
currentSHA,
newPackageContents,
commitMessage);
})
.catch((error) => {
logger.error(`${depName} ensureCommit error: ${error}`);
throw error;
});
}
......
const chai = require('chai');
const fs = require('fs');
const npm = require('../../app/helpers/npm');
const winston = require('winston');
chai.should();
npm.setLogger(winston);
const inputContent = fs.readFileSync('./test/_fixtures/package.json/inputs/01.json', 'utf8');
describe('npm helper', () => {
describe('extractDependencies', () => {
const extractedDependencies = npm.extractDependencies(JSON.parse(inputContent));
it('returns an array of correct length', () => {
extractedDependencies.should.be.instanceof(Array);
extractedDependencies.should.have.length(10);
});
it('each element contains non-null depType, depName, currentVersion', () => {
extractedDependencies.every(dep => dep.depType && dep.depName && dep.currentVersion)
.should.eql(true);
});
});
describe('isRange', () => {
it('should reject simple semver', () => {
npm.isRange('1.2.3').should.eql(false);
});
it('should support tilde', () => {
npm.isRange('~1.2.3').should.eql(true);
});
it('should support caret', () => {
npm.isRange('^1.2.3').should.eql(true);
});
});
describe('isValidVersion', () => {
it('should support simple semver', () => {
npm.isValidVersion('1.2.3').should.eql(true);
});
it('should support versions with dash', () => {
npm.isValidVersion('1.2.3-foo').should.eql(true);
});
it('should reject versions without dash', () => {
npm.isValidVersion('1.2.3foo').should.eql(false);
});
it('should support ranges', () => {
npm.isValidVersion('~1.2.3').should.eql(true);
npm.isValidVersion('^1.2.3').should.eql(true);
npm.isValidVersion('>1.2.3').should.eql(true);
});
it('should reject github repositories', () => {
npm.isValidVersion('singapore/renovate').should.eql(false);
npm.isValidVersion('singapore/renovate#master').should.eql(false);
npm.isValidVersion('https://github.com/singapore/renovate.git').should.eql(false);
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment