Skip to content
Snippets Groups Projects
Select Git revision
  • a0ed1c2037f716c475e351ca85dace823f2bc4bd
  • main default protected
  • dependabot/go_modules/helm.sh/helm/v3-3.18.4
  • release/v2.6.x
  • dependabot/github_actions/ci-641206964f
  • conform-k8s-1.33
  • rfc-external-artifact
  • release/v2.5.x
  • release/v2.4.x
  • remove-notation-validation
  • release/v2.3.x
  • release/v2.2.x
  • RFC
  • fix-commit-log
  • flux-audit
  • release/v2.1.x
  • context-ns
  • ksm-dashboard
  • rfc-passwordless-git-auth
  • release/v2.0.x
  • rfc-gating
  • v2.6.4 protected
  • v2.6.3 protected
  • v2.6.2 protected
  • v2.6.1 protected
  • v2.6.0 protected
  • v2.5.1 protected
  • v2.5.0 protected
  • v2.4.0 protected
  • v2.3.0 protected
  • v2.2.3 protected
  • v2.2.2 protected
  • v2.2.1 protected
  • v2.2.0 protected
  • v2.1.2 protected
  • v2.1.1 protected
  • v2.1.0 protected
  • v2.0.1 protected
  • v2.0.0 protected
  • v2.0.0-rc.5 protected
  • v2.0.0-rc.4 protected
41 results

create_source.go

Blame
  • github.js 21.09 KiB
    let logger = require('../logger');
    const ghGot = require('gh-got');
    
    const config = {};
    
    module.exports = {
      // GitHub App
      getInstallations,
      getInstallationToken,
      getInstallationRepositories,
      // Initialization
      getRepos,
      initRepo,
      setBaseBranch,
      // Search
      findFilePaths,
      // Branch
      branchExists,
      getAllRenovateBranches,
      isBranchStale,
      getBranchPr,
      getBranchStatus,
      deleteBranch,
      mergeBranch,
      // issue
      addAssignees,
      addReviewers,
      addLabels,
      // PR
      findPr,
      checkForClosedPr,
      createPr,
      getPr,
      getAllPrs,
      updatePr,
      mergePr,
      // file
      getSubDirectories,
      commitFilesToBranch,
      getFile,
      getFileContent,
      getFileJson,
      // Commits
      getCommitMessages,
    };
    
    // Get all installations for a GitHub app
    async function getInstallations(appToken) {
      logger.debug('getInstallations(appToken)');
      try {
        const url = 'app/installations';
        const options = {
          headers: {
            accept: 'application/vnd.github.machine-man-preview+json',
            authorization: `Bearer ${appToken}`,
          },
        };
        const res = await ghGot(url, options);
        logger.debug(`Returning ${res.body.length} results`);
        return res.body;
      } catch (err) {
        logger.error({ err }, `GitHub getInstallations error`);
        throw err;
      }
    }
    
    // Get the user's installation token
    async function getInstallationToken(appToken, installationId) {
      logger.debug(`getInstallationToken(appToken, ${installationId})`);
      try {
        const url = `installations/${installationId}/access_tokens`;
        const options = {
          headers: {
            accept: 'application/vnd.github.machine-man-preview+json',
            authorization: `Bearer ${appToken}`,
          },
        };
        const res = await ghGot.post(url, options);
        return res.body.token;
      } catch (err) {
        logger.error({ err }, `GitHub getInstallationToken error`);
        throw err;
      }
    }
    
    // Get all repositories for a user's installation
    async function getInstallationRepositories(userToken) {
      logger.debug('getInstallationRepositories(userToken)');
      try {
        const url = 'installation/repositories';
        const options = {
          headers: {
            accept: 'application/vnd.github.machine-man-preview+json',
            authorization: `token ${userToken}`,
          },
        };
        const res = await ghGot(url, options);
        logger.debug(
          `Returning ${res.body.repositories.length} results from a total of ${res
            .body.total_count}`
        );
        return res.body;
      } catch (err) {
        logger.error({ err }, `GitHub getInstallationRepositories error`);
        throw err;
      }
    }
    
    // Get all repositories that the user has access to
    async function getRepos(token, endpoint) {
      logger.debug('getRepos(token, endpoint)');
      if (token) {
        process.env.GITHUB_TOKEN = token;
      } else if (!process.env.GITHUB_TOKEN) {
        throw new Error('No token found for getRepos');
      }
      if (endpoint) {
        process.env.GITHUB_ENDPOINT = endpoint;
      }
      try {
        const res = await ghGot('user/repos');
        return res.body.map(repo => repo.full_name);
      } catch (err) /* istanbul ignore next */ {
        logger.error({ err }, `GitHub getRepos error`);
        throw err;
      }
    }
    
    // Initialize GitHub by getting base branch and SHA
    async function initRepo(repoName, token, endpoint, repoLogger) {
      logger = repoLogger || logger;
      logger.debug(`initRepo("${repoName}")`);
      if (repoLogger) {
        logger = repoLogger;
      }
      if (token) {
        process.env.GITHUB_TOKEN = token;
      } else if (!process.env.GITHUB_TOKEN) {
        throw new Error(`No token found for GitHub repository ${repoName}`);
      }
      if (endpoint) {
        process.env.GITHUB_ENDPOINT = endpoint;
      }
      config.repoName = repoName;
      const platformConfig = {};
      try {
        const res = await ghGot(`repos/${repoName}`);
        config.privateRepo = res.body.private === true;
        config.owner = res.body.owner.login;
        logger.debug(`${repoName} owner = ${config.owner}`);
        // Use default branch as PR target unless later overridden
        config.defaultBranch = res.body.default_branch;
        config.baseBranch = config.defaultBranch;
        logger.debug(`${repoName} default branch = ${config.baseBranch}`);
        config.baseCommitSHA = await getBranchCommit(config.baseBranch);
        if (res.body.allow_rebase_merge) {
          config.mergeMethod = 'rebase';
        } else if (res.body.allow_squash_merge) {
          config.mergeMethod = 'squash';
        } else if (res.body.allow_merge_commit) {
          config.mergeMethod = 'merge';
        } else {
          logger.debug('Could not find allowed merge methods for repo');
        }
        platformConfig.repoForceRebase = false;
        try {
          const branchProtection = await getBranchProtection(config.baseBranch);
          if (branchProtection.strict) {
            logger.debug('Repo has branch protection and needs PRs up-to-date');
            platformConfig.repoForceRebase = true;
          } else {
            logger.debug(
              'Repo has branch protection but does not require up-to-date'
            );
          }
        } catch (err) {
          if (err.statusCode === 404) {
            logger.debug('Repo has no branch protection');
          } else if (err.statusCode === 403) {
            logger.debug('Do not have permissions to detect branch protection');
          } else {
            throw err;
          }
        }
      } catch (err) /* istanbul ignore next */ {
        if (err.statusCode === 409) {
          logger.debug('Repository is not initiated');
          throw new Error('uninitiated');
        }
        logger.error({ err }, 'Unknown GitHub initRepo error');
        throw err;
      }
      return platformConfig;
    }
    
    async function getBranchProtection(branchName) {
      const res = await ghGot(
        `repos/${config.repoName}/branches/${branchName}/protection/required_status_checks`,
        {
          headers: {
            accept: 'application/vnd.github.loki-preview+json',
          },
        }
      );
      return res.body;
    }
    
    async function setBaseBranch(branchName) {
      if (branchName) {
        logger.debug(`Setting baseBranch to ${branchName}`);
        config.baseBranch = branchName;
        config.baseCommitSHA = await getBranchCommit(config.baseBranch);
      }
    }
    
    // Search
    
    // Returns an array of file paths in current repo matching the fileName
    async function findFilePaths(fileName) {
      const res = await ghGot(
        `search/code?q=repo:${config.repoName}+filename:${fileName}`
      );
      const exactMatches = res.body.items.filter(item => item.name === fileName);
      // GitHub seems to return files in the root with a leading `/`
      // which then breaks things later on down the line
      return exactMatches.map(item => item.path.replace(/^\//, ''));
    }
    
    // Branch
    
    // Returns true if branch exists, otherwise false
    async function branchExists(branchName) {
      logger.debug(`Checking if branch exists: ${branchName}`);
      try {
        const res = await ghGot(
          `repos/${config.repoName}/git/refs/heads/${branchName}`
        );
        if (Array.isArray(res.body)) {
          // This seems to happen if GitHub has partial matches, so we check ref
          const matchedBranch = res.body.some(
            branch => branch.ref === `refs/heads/${branchName}`
          );
          if (matchedBranch) {
            logger.debug('Branch exists');
          } else {
            logger.debug('No matching branches');
          }
          return matchedBranch;
        }
        // This should happen if there's an exact match
        return res.body.ref === `refs/heads/${branchName}`;
      } catch (error) {
        if (error.statusCode === 404) {
          // If file not found, then return false
          logger.debug("Branch doesn't exist");
          return false;
        }
        // Propagate if it's any other error
        throw error;
      }
    }
    
    async function getAllRenovateBranches() {
      logger.trace('getAllRenovateBranches');
      const allBranches = (await ghGot(`repos/${config.repoName}/git/refs/heads`))
        .body;
      return allBranches.reduce((arr, branch) => {
        if (branch.ref.indexOf('refs/heads/renovate/') === 0) {
          arr.push(branch.ref.substring('refs/heads/'.length));
        }
        return arr;
      }, []);
    }
    
    async function isBranchStale(branchName) {
      // Check if branch's parent SHA = master SHA
      logger.debug(`isBranchStale(${branchName})`);
      const branchCommit = await getBranchCommit(branchName);
      logger.debug(`branchCommit=${branchCommit}`);
      const commitDetails = await getCommitDetails(branchCommit);
      logger.debug({ commitDetails }, `commitDetails`);
      const parentSha = commitDetails.parents[0].sha;
      logger.debug(`parentSha=${parentSha}`);
      // Return true if the SHAs don't match
      return parentSha !== config.baseCommitSHA;
    }
    
    // Returns the Pull Request for a branch. Null if not exists.
    async function getBranchPr(branchName) {
      logger.debug(`getBranchPr(${branchName})`);
      const gotString =
        `repos/${config.repoName}/pulls?` +
        `state=open&base=${config.baseBranch}&head=${config.owner}:${branchName}`;
      const res = await ghGot(gotString);
      if (!res.body.length) {
        return null;
      }
      const prNo = res.body[0].number;
      return getPr(prNo);
    }
    
    // Returns the combined status for a branch.
    async function getBranchStatus(branchName, requiredStatusChecks) {
      logger.debug(`getBranchStatus(${branchName})`);
      if (!requiredStatusChecks) {
        // null means disable status checks, so it always succeeds
        return 'success';
      }
      if (requiredStatusChecks.length) {
        // This is Unsupported
        logger.warn({ requiredStatusChecks }, `Unsupported requiredStatusChecks`);
        return 'failed';
      }
      const gotString = `repos/${config.repoName}/commits/${branchName}/status`;
      logger.debug(gotString);
      const res = await ghGot(gotString);
      return res.body.state;
    }
    
    async function deleteBranch(branchName) {
      await ghGot.delete(`repos/${config.repoName}/git/refs/heads/${branchName}`);
    }
    
    async function mergeBranch(branchName, mergeType) {
      logger.debug(`mergeBranch(${branchName}, ${mergeType})`);
      if (mergeType === 'branch-push') {
        const url = `repos/${config.repoName}/git/refs/heads/${config.baseBranch}`;
        const options = {
          body: {
            sha: await getBranchCommit(branchName),
          },
        };
        try {
          await ghGot.patch(url, options);
        } catch (err) {
          logger.error({ err }, `Error pushing branch merge for ${branchName}`);
          throw new Error('branch-push failed');
        }
      } else if (mergeType === 'branch-merge-commit') {
        const url = `repos/${config.repoName}/merges`;
        const options = {
          body: {
            base: config.baseBranch,
            head: branchName,
          },
        };
        try {
          await ghGot.post(url, options);
        } catch (err) {
          logger.error({ err }, `Error pushing branch merge for ${branchName}`);
          throw new Error('branch-push failed');
        }
      } else {
        throw new Error(`Unsupported branch merge type: ${mergeType}`);
      }
      // Update base commit
      config.baseCommitSHA = await getBranchCommit(config.baseBranch);
      // Delete branch
      await deleteBranch(branchName);
    }
    
    // Issue
    
    async function addAssignees(issueNo, assignees) {
      logger.debug(`Adding assignees ${assignees} to #${issueNo}`);
      await ghGot.post(`repos/${config.repoName}/issues/${issueNo}/assignees`, {
        body: {
          assignees,
        },
      });
    }
    
    async function addReviewers(issueNo, reviewers) {
      logger.debug(`Adding reviewers ${reviewers} to #${issueNo}`);
      await ghGot.post(
        `repos/${config.repoName}/pulls/${issueNo}/requested_reviewers`,
        {
          headers: {
            accept: 'application/vnd.github.black-cat-preview+json',
          },
          body: {
            reviewers,
          },
        }
      );
    }
    
    async function addLabels(issueNo, labels) {
      logger.debug(`Adding labels ${labels} to #${issueNo}`);
      await ghGot.post(`repos/${config.repoName}/issues/${issueNo}/labels`, {
        body: labels,
      });
    }
    
    async function findPr(branchName, prTitle, state = 'all') {
      logger.debug(`findPr(${branchName}, ${state})`);
      const urlString = `repos/${config.repoName}/pulls?head=${config.owner}:${branchName}&state=${state}`;
      logger.debug(`findPr urlString: ${urlString}`);
      const res = await ghGot(urlString);
      let pr = null;
      res.body.forEach(result => {
        if (!prTitle || result.title === prTitle) {
          pr = result;
          if (pr.state === 'closed') {
            pr.isClosed = true;
          }
          pr.displayNumber = `Pull Request #${pr.number}`;
        }
      });
      return pr;
    }
    
    // Pull Request
    async function checkForClosedPr(branchName, prTitle) {
      logger.debug(`checkForClosedPr(${branchName}, ${prTitle})`);
      const url = `repos/${config.repoName}/pulls?state=closed&head=${config.owner}:${branchName}`;
      const res = await ghGot(url);
      // Return true if any of the titles match exactly
      return res.body.some(
        pr =>
          pr.title === prTitle && pr.head.label === `${config.owner}:${branchName}`
      );
    }
    
    // Creates PR and returns PR number
    async function createPr(branchName, title, body, useDefaultBranch) {
      const base = useDefaultBranch ? config.defaultBranch : config.baseBranch;
      const pr = (await ghGot.post(`repos/${config.repoName}/pulls`, {
        body: {
          title,
          head: branchName,
          base,
          body,
        },
      })).body;
      pr.displayNumber = `Pull Request #${pr.number}`;
      return pr;
    }
    
    // Gets details for a PR
    async function getPr(prNo) {
      if (!prNo) {
        return null;
      }
      const pr = (await ghGot(`repos/${config.repoName}/pulls/${prNo}`)).body;
      if (!pr) {
        return null;
      }
      // Harmonise PR values
      pr.displayNumber = `Pull Request #${pr.number}`;
      if (pr.state === 'closed') {
        pr.isClosed = true;
      }
      if (!pr.isClosed) {
        if (pr.mergeable_state === 'dirty') {
          logger.debug(`PR mergeable state is dirty`);
          pr.isUnmergeable = true;
        }
        if (pr.commits === 1) {
          // Only one commit was made - must have been renovate
          logger.debug('Only 1 commit in PR so rebase is possible');
          pr.canRebase = true;
        } else {
          // Check if only one author of all commits
          logger.debug('Checking all commits');
          const prCommits = (await ghGot(
            `repos/${config.repoName}/pulls/${prNo}/commits`
          )).body;
          const authors = prCommits.reduce((arr, commit) => {
            logger.trace({ commit }, `Checking commit`);
            let author = 'unknown';
            if (commit.author) {
              author = commit.author.login;
            } else if (commit.commit && commit.commit.author) {
              author = commit.commit.author.email;
            } else {
              logger.debug('Could not determine commit author');
            }
            logger.debug(`Commit author is: ${author}`);
            if (arr.indexOf(author) === -1) {
              arr.push(author);
            }
            return arr;
          }, []);
          logger.debug(`Author list: ${authors}`);
          if (authors.length === 1) {
            pr.canRebase = true;
          }
        }
        if (pr.base.sha !== config.baseCommitSHA) {
          pr.isStale = true;
        }
      }
      return pr;
    }
    
    async function getAllPrs() {
      const all = (await ghGot(`repos/${config.repoName}/pulls?state=open`)).body;
      return all.map(pr => ({
        number: pr.number,
        branchName: pr.head.ref,
      }));
    }
    
    async function updatePr(prNo, title, body) {
      await ghGot.patch(`repos/${config.repoName}/pulls/${prNo}`, {
        body: { title, body },
      });
    }
    
    async function mergePr(pr) {
      const url = `repos/${config.repoName}/pulls/${pr.number}/merge`;
      const options = {
        body: {},
      };
      if (config.mergeMethod) {
        // This path is taken if we have auto-detected the allowed merge types from the repo
        options.body.merge_method = config.mergeMethod;
        try {
          logger.debug({ options, url }, `mergePr`);
          await ghGot.put(url, options);
        } catch (err) {
          logger.error({ err }, `Failed to ${options.body.merge_method} PR`);
          return;
        }
      } else {
        // We need to guess the merge method and try squash -> rebase -> merge
        options.body.merge_method = 'rebase';
        try {
          logger.debug({ options, url }, `mergePr`);
          await ghGot.put(url, options);
        } catch (err1) {
          logger.debug({ err: err1 }, `Failed to ${options.body.merge_method} PR}`);
          try {
            options.body.merge_method = 'squash';
            logger.debug({ options, url }, `mergePr`);
            await ghGot.put(url, options);
          } catch (err2) {
            logger.debug(
              { err: err2 },
              `Failed to ${options.body.merge_method} PR`
            );
            try {
              options.body.merge_method = 'merge';
              logger.debug({ options, url }, `mergePr`);
              await ghGot.put(url, options);
            } catch (err3) {
              logger.debug(
                { err: err3 },
                `Failed to ${options.body.merge_method} PR`
              );
              logger.error('All merge attempts failed');
              return;
            }
          }
        }
      }
      // Update base branch SHA
      config.baseCommitSHA = await getBranchCommit(config.baseBranch);
      // Delete branch
      await deleteBranch(pr.head.ref);
    }
    
    // Generic File operations
    
    async function getFile(filePath, branchName = config.baseBranch) {
      const res = await ghGot(
        `repos/${config.repoName}/contents/${filePath}?ref=${branchName}`
      );
      return res.body.content;
    }
    
    async function getFileContent(filePath, branchName = config.baseBranch) {
      logger.trace(
        `getFileContent(filePath=${filePath}, branchName=${branchName})`
      );
      try {
        const file = await getFile(filePath, branchName);
        return new Buffer(file, 'base64').toString();
      } catch (error) {
        if (error.statusCode === 404) {
          // If file not found, then return null JSON
          return null;
        }
        // Propagate if it's any other error
        throw error;
      }
    }
    
    async function getFileJson(filePath, branchName) {
      logger.trace(`getFileJson(filePath=${filePath}, branchName=${branchName})`);
      let fileJson = null;
      try {
        fileJson = JSON.parse(await getFileContent(filePath, branchName));
      } catch (err) {
        logger.error({ err }, `Failed to parse JSON for ${filePath}`);
      }
      return fileJson;
    }
    
    async function getSubDirectories(path) {
      logger.trace(`getSubDirectories(path=${path})`);
      const res = await ghGot(`repos/${config.repoName}/contents/${path}`);
      const directoryList = [];
      res.body.forEach(item => {
        if (item.type === 'dir') {
          directoryList.push(item.name);
        }
      });
      return directoryList;
    }
    
    // Add a new commit, create branch if not existing
    async function commitFilesToBranch(
      branchName,
      files,
      message,
      parentBranch = config.baseBranch
    ) {
      logger.debug(
        `commitFilesToBranch('${branchName}', files, message, '${parentBranch})'`
      );
      const parentCommit = await getBranchCommit(parentBranch);
      const parentTree = await getCommitTree(parentCommit);
      const fileBlobs = [];
      // Create blobs
      for (const file of files) {
        const blob = await createBlob(file.contents);
        fileBlobs.push({
          name: file.name,
          blob,
        });
      }
      // Create tree
      const tree = await createTree(parentTree, fileBlobs);
      const commit = await createCommit(parentCommit, tree, message);
      const isBranchExisting = await branchExists(branchName);
      if (isBranchExisting) {
        await updateBranch(branchName, commit);
      } else {
        await createBranch(branchName, commit);
      }
    }
    
    // Internal branch operations
    
    // Creates a new branch with provided commit
    async function createBranch(branchName, commit = config.baseCommitSHA) {
      await ghGot.post(`repos/${config.repoName}/git/refs`, {
        body: {
          ref: `refs/heads/${branchName}`,
          sha: commit,
        },
      });
    }
    
    // Internal: Updates an existing branch to new commit sha
    async function updateBranch(branchName, commit) {
      logger.debug(`Updating branch ${branchName} with commit ${commit}`);
      await ghGot.patch(`repos/${config.repoName}/git/refs/heads/${branchName}`, {
        body: {
          sha: commit,
          force: true,
        },
      });
    }
    
    // Low-level commit operations
    
    // Create a blob with fileContents and return sha
    async function createBlob(fileContents) {
      logger.debug('Creating blob');
      return (await ghGot.post(`repos/${config.repoName}/git/blobs`, {
        body: {
          encoding: 'base64',
          content: new Buffer(fileContents).toString('base64'),
        },
      })).body.sha;
    }
    
    // Return the commit SHA for a branch
    async function getBranchCommit(branchName) {
      return (await ghGot(`repos/${config.repoName}/git/refs/heads/${branchName}`))
        .body.object.sha;
    }
    
    async function getCommitDetails(commit) {
      logger.debug(`getCommitDetails(${commit})`);
      const results = await ghGot(`repos/${config.repoName}/git/commits/${commit}`);
      return results.body;
    }
    
    // Return the tree SHA for a commit
    async function getCommitTree(commit) {
      logger.debug(`getCommitTree(${commit})`);
      return (await ghGot(`repos/${config.repoName}/git/commits/${commit}`)).body
        .tree.sha;
    }
    
    // Create a tree and return SHA
    async function createTree(baseTree, files) {
      logger.debug(`createTree(${baseTree}, files)`);
      const body = {
        base_tree: baseTree,
        tree: [],
      };
      files.forEach(file => {
        body.tree.push({
          path: file.name,
          mode: '100644',
          type: 'blob',
          sha: file.blob,
        });
      });
      logger.trace({ body }, 'createTree body');
      return (await ghGot.post(`repos/${config.repoName}/git/trees`, { body })).body
        .sha;
    }
    
    // Create a commit and return commit SHA
    async function createCommit(parent, tree, message) {
      logger.debug(`createCommit(${parent}, ${tree}, ${message})`);
      return (await ghGot.post(`repos/${config.repoName}/git/commits`, {
        body: {
          message,
          parents: [parent],
          tree,
        },
      })).body.sha;
    }
    
    async function getCommitMessages() {
      logger.debug('getCommitMessages');
      try {
        const res = await ghGot(`repos/${config.repoName}/commits`);
        return res.body.map(commit => commit.commit.message);
      } catch (err) {
        logger.error({ err }, `getCommitMessages error`);
        return [];
      }
    }