Skip to content
Snippets Groups Projects
Select Git revision
  • 00a176e95bee44b048b1ccb2f42687b35e120186
  • master default
2 results

index.js

Blame
  • user avatar
    Rhys Arkins authored
    00a176e9
    History
    index.js 8.88 KiB
    // Set colours for console globally
    // eslint-disable-next-line no-unused-expressions
    require('manakin').global;
    
    const semver = require('semver');
    
    const github = require('./helpers/github');
    const npm = require('./helpers/npm');
    const packageJson = require('./helpers/packageJson');
    
    const config = initConfig();
    validateArguments();
    npm.init(config);
    
    // Initialize our promise chain
    let p = Promise.resolve();
    
    // Queue up each repo/package combination
    config.repositories.forEach((repo) => {
      const repoName = repo.name;
      repo.packageFiles.forEach((packageFile) => {
        p = p.then(() => processRepoPackageFile(repoName, packageFile));
      });
    });
    // Print something nice once the chain is done
    p
      .then(() => {
        // eslint-disable-next-line promise/always-return
        if (config.repositories.length > 1) {
          console.success('All repos done');
        }
      })
      .catch((error) => {
        console.log(`Unexpected error: ${error}`);
      });
    
    // This function reads in all configs and merges them
    function initConfig() {
      /* eslint-disable global-require */
      const defaultConfig = require('./defaults');
      let customConfig = {};
      try {
        customConfig = require('./config');
      } catch (err) {
        // Do nothing
      }
      /* eslint-enable global-require */
      const cliConfig = {};
      if (process.env.RENOVATE_TOKEN) {
        cliConfig.token = process.env.RENOVATE_TOKEN;
      }
      // Check if repository name and package file are provided via CLI
      const repoName = process.argv[2];
      const packageFile = process.argv[3] || 'package.json';
      if (repoName) {
        cliConfig.repositories = [
          {
            name: repoName,
            packageFiles: [packageFile],
          },
        ];
      }
      const combinedConfig = Object.assign(defaultConfig, customConfig, cliConfig);
      // First, convert any strings to objects
      combinedConfig.repositories.forEach((repo, index) => {
        if (typeof repo === 'string') {
          combinedConfig.repositories[index] = { name: repo };
        }
      });
      // Add 'package.json' if missing
      combinedConfig.repositories.forEach((repo, index) => {
        if (!repo.packageFiles || !repo.packageFiles.length) {
          combinedConfig.repositories[index].packageFiles = ['package.json'];
        }
      });
      if (combinedConfig.verbose) {
        console.log(`config = ${JSON.stringify(combinedConfig)}`);
      }
      return combinedConfig;
    }
    
    // This function makes sure we have a token and at least one repository
    function validateArguments() {
      // token must be defined
      if (typeof config.token === 'undefined') {
        console.error('Error: A GitHub token must be configured');
        process.exit(1);
      }
      // We also need a repository
      if (!config.repositories || config.repositories.length === 0) {
        console.error('Error: At least one repository must be configured');
      }
    }
    
    // This function manages the queue per-package file
    function processRepoPackageFile(repoName, packageFile) {
      return initGitHub(repoName, packageFile)
        .then(getPackageFileContents)
        .then(determineUpgrades)
        .then(processUpgradesSequentially)
        // eslint-disable-next-line promise/always-return
        .then(() => {
          console.success(`Repo ${repoName} ${packageFile} done`);
        })
        .catch((error) => {
          console.error(`renovate caught error: ${error}`);
        });
    }
    
    function initGitHub(repoName, packageFile) {
      console.info(`Initializing GitHub repo ${repoName}, ${packageFile}`);
      return github.init(config, repoName, packageFile);
    }
    
    function getPackageFileContents() {
      console.info('Getting package file contents');
      return github.getPackageFileContents();
    }
    
    function determineUpgrades(packageFileContents) {
      console.info('Determining required upgrades');
      return npm.getAllDependencyUpgrades(packageFileContents);
    }
    
    function processUpgradesSequentially(upgrades) {
      if (Object.keys(upgrades).length) {
        console.info('Processing upgrades');
      } else {
        console.info('No upgrades to process');
      }
      if (config.verbose) {
        console.log(`All upgrades: ${JSON.stringify(upgrades)}`);
      }
      // We are processing each upgrade sequentially for two major reasons:
      // 1. Reduce chances of GitHub API rate limiting
      // 2. Edge case collision of branch name, e.g. dependency also listed as dev dependency
      return upgrades.reduce(
        (promise, upgrade) => promise.then(() => updateDependency(upgrade)), Promise.resolve());
    }
    
    function updateDependency({ upgradeType, depType, depName, currentVersion, newVersion }) {
      const newVersionMajor = semver.major(newVersion);
      const branchName = config.templates.branchName({
        depType,
        depName,
        currentVersion,
        newVersion,
        newVersionMajor,
      });
      let prTitle = '';
      if (upgradeType === 'pin') {
        prTitle = config.templates.prTitlePin({
          depType,
          depName,
          currentVersion,
          newVersion,
          newVersionMajor,
        });
      } else if (upgradeType === 'minor') {
        // Use same title for range or minor
        prTitle = config.templates.prTitleMinor({
          depType,
          depName,
          currentVersion,
          newVersion,
          newVersionMajor,
        });
      } else {
        prTitle = config.templates.prTitleMajor({
          depType,
          depName,
          currentVersion,
          newVersion,
          newVersionMajor,
        });
      }
      const prBody = config.templates.prBody({
        depName,
        currentVersion,
        newVersion,
      });
      const commitMessage = config.templates.commitMessage({
        depName,
        currentVersion,
        newVersion,
      });
    
      // Check if same PR already existed and skip if so
      // This allows users to close an unwanted upgrade PR and not worry about seeing it raised again
      return github.checkForClosedPr(branchName, prTitle).then((prExisted) => {
        if (prExisted) {
          if (config.verbose) {
            console.log(`${depName}: Skipping due to existing PR found.`);
          }
          return Promise.resolve();
        }
        return ensureAll();
      });
      function ensureAll() {
        return ensureBranch()
        .then(ensureCommit)
        .then(ensurePr)
        .catch((error) => {
          console.error(`Error updating dependency ${depName}:  ${error}`);
          // Don't throw here - we don't want to stop the other renovations
        });
      }
      function ensureBranch() {
        // Save an API call by attempting to create branch without checking for existence first
        return github.createBranch(branchName).catch((error) => {
          // Check in case it's because the branch already existed
          if (error.response.body.message !== 'Reference already exists') {
            // In this case it means we really do have a problem and can't continue
            console.error(`Error creating branch: ${branchName}`);
            console.error(`Response body: ${error.response.body}`);
            throw error;
          }
          // Otherwise we swallow this error and continue
        });
      }
      function ensureCommit() {
        // Retrieve the package.json from this renovate branch
        return github.getPackageFile(branchName).then((res) => {
          const currentSHA = res.body.sha;
          const currentFileContent = new Buffer(
            res.body.content,
            'base64',
          ).toString();
          const currentJson = JSON.parse(currentFileContent);
          if (currentJson[depType][depName] === newVersion) {
            if (config.verbose) {
              console.log(`${depName}: branch ${branchName} is already up-to-date`);
            }
            return Promise.resolve();
          }
          // Branch must need updating
          if (config.verbose) {
            console.log(
              `${depName}: Updating to ${newVersion} in branch ${branchName}`,
            );
          }
          const newPackageContents = packageJson.setNewValue(
            currentFileContent,
            depType,
            depName,
            newVersion,
          );
          return github.writePackageFile(
            branchName,
            currentSHA,
            newPackageContents,
            commitMessage,
          );
        });
      }
    
      // Ensures that PR exists with matching title/body
      function ensurePr() {
        // Create PR based on current state
        function createPr() {
          return github.createPr(branchName, prTitle, prBody).then((newPr) => {
            console.info(`${depName}: Created PR #${newPr.number}`);
            return Promise.resolve();
          });
        }
        // Update PR based on current state
        function updatePr(existingPr) {
          return github.updatePr(existingPr.number, prTitle, prBody).then(() => {
            console.info(`${depName}: Updated PR #${existingPr.number}`);
            return Promise.resolve();
          });
        }
        // Process a returned PR
        function processExistingPr(existingPr) {
          if (!existingPr) {
            // We need to create a new PR
            return createPr();
          }
          // Check if existing PR needs updating
          if (existingPr.title === prTitle || existingPr.body === prBody) {
            if (config.verbose) {
              console.log(
                `${depName}: PR #${existingPr.number} already up-to-date`,
              );
            }
            return Promise.resolve();
          }
          // PR must need updating
          return updatePr(existingPr);
        }
    
        return github.getPr(branchName)
        .then(processExistingPr)
        .catch((error) => {
          console.log(`${depName} failed to ensure PR: ${error}`);
        });
      }
    }