function Cleaner()

in preview/clean.js [13:188]


function Cleaner(token, repo, cache_dir, tmp_dir) {
  let repo_name = path.basename(repo);
  if (!repo_name.endsWith('.git')) {
    repo_name += '.git';
  }
  const local_path = `${tmp_dir}/${repo_name}`;

  const clone = () => {
    return exec_git([
      'clone',
      '--bare',
      '--reference', `${cache_dir}/${repo_name}`,
      repo, local_path])
  }

  const show_heads = () => {
    return exec_git(['show-ref', '--heads'], { cwd: local_path });
  }

  const heads_to_prs = (heads) => {
    return heads
      .split('\n')
      .reduce((acc, line) => {
        const found = line.match(/^.+ refs\/heads\/((.+)_(\d+))$/);
        if (found) {
          acc.push({
            branch: found[1],
            // Temporary hack during the Buildkite migration that breaks the <repo>_<PR> convention,
            // and introduces the <repo>_BK_<PR> convention - see https://github.com/elastic/docs-projects/issues/134
            repo: found[2].includes('_bk') ? found[2].replace('_bk', '') : found[2],
            number: Number(found[3]),
          });
        }
        return acc;
      }, [])
      .sort((lhs, rhs) => {
        if (lhs.repo < rhs.repo) {
          return -1;
        }
        if (lhs.repo > rhs.repo) {
          return 1;
        }
        return lhs.number - rhs.number;
      });
  }

  const cleanup_closed_prs = async prs => {
    const now = Date.now() / 1000;
    for (let pr of prs) {
      const url = `https://www.github.com/elastic/${pr.repo}/pull/${pr.number}`;
      const age = await prAge(pr);
      const days = (now - age) / 24 / 60 / 60;
      if (days > EXPIRE_DAYS) {
        console.log(`Deleting ${pr.branch} for ${days.toFixed(2)} days old pr at ${url}`);
        deleteBranch(pr);
      } else if (await is_pr_closed(pr)) {
        console.info(`Deleting ${pr.branch} for closed pr at ${url}`);
        deleteBranch(pr);
      } else {
        console.info(`Preserving ${pr.branch} for open pr at ${url}`);
      }
    }
  }

  const prAge = async function (pr) {
    return parseInt(await exec_git(
      [
        "show", "--pretty=%ad", "--no-notes", "--no-patch", "--date=unix",
        pr.branch
      ],
      { cwd: local_path }
    ));
  }

  const deleteBranch = async function (pr) {
    if (pr.branch === 'master' || pr.branch === 'staging') {
      // Just for super double ultra paranoia.
      throw "Can't delete master!";
    }
    await exec_git(
      ['push', 'origin', '--delete', pr.branch],
      { cwd: local_path }
    );
  }

  const is_pr_closed = function (pr) {
    console.info(`Checking status of https://github.com/elastic/${pr.repo}/pull/${pr.number}`);
    return new Promise((resolve, reject) => {
      const body = {
        query: `
          query PRClosed($repo: String!, $number: Int!) {
            repository(owner:\"elastic\", name:$repo) {
              pullRequest(number:$number) {
                closed
              }
            }
          }
        `,
        variables: {
          repo: pr.repo,
          number: pr.number,
        },
      };
      const postData = JSON.stringify(body);
      const req = https.request({
        method: 'POST',
        host: 'api.github.com',
        path: '/graphql',
        headers: {
          'Authorization': `bearer ${token}`,
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(postData),
          'User-Agent': 'Elastic Docs Preview Cleaner Upper',
        }
      }, res => {
        let data = '';
        res.on('data', chunk => {
          data += chunk;
        });
        res.on('end', () => {
          if (res.statusCode !== 200) {
            reject(`Error getting ${JSON.stringify(pr)} [${res.statusCode}]:\n${data}`);
          } else {
            let closed;
            try {
              const parsed = JSON.parse(data);
              const repo = parsed.data.repository;
              if (repo) {
                closed = repo.pullRequest.closed;
              } else {
                console.warn(pr.branch,
                  "looks like a PR but isn't for a repo we manage so we assume",
                  'it is open');
                closed = false;
              }
            } catch (e) {
              reject(e);
              return;
            }
            if (closed === undefined) {
              reject(`unexpected reply from github:${data}`);
              return;
            }
            if (res.headers['x-ratelimit-remaining'] < 100) {
              const until = res.headers['x-ratelimit-reset'];
              const millis = until * 1000 - Date.now().getTime();
              console.info('Rate limited for', millis, 'milliseconds');
              setTimeout(() => resolve(closed), millis);
            } else {
              resolve(closed);
            }
          }
        });
      });
      req.on('error', err => {
        reject(`Error getting ${JSON.stringify(pr)}:\n${err}`);
      });
      req.write(postData);
      req.end();
    });
  }

  return {
    run: () => {
      return clone()
        .then(show_heads)
        .then(heads_to_prs)
        .then(cleanup_closed_prs);
    },
    clone: clone,
    is_pr_closed: is_pr_closed,

    repo: repo,
    local_path: local_path,
  };
}