@ 261105.4
current
import { FormData } from 'undici';

import {
  JobParser,
  CI_DOMAIN,
  CI_TYPES,
  CI_TYPES_KEYS
} from './ci_type_parser.js';
import PRData from '../pr_data.js';
import { debuglog } from '../verbosity.js';
import PRChecker from '../pr_checker.js';
import { PRBuild } from './build-types/pr_build.js';
import { getCIActionAdvice } from './ci_utils.js';
import {
  findResumableBuild,
  getApprovedSHA,
  RESUME_TREE
} from './resumable_build.js';

export const CI_CRUMB_URL = `https://${CI_DOMAIN}/crumbIssuer/api/json`;
const CI_PR_NAME = CI_TYPES.get(CI_TYPES_KEYS.PR).jobName;
export const CI_PR_URL = `https://${CI_DOMAIN}/job/${CI_PR_NAME}/build`;

const CI_V8_NAME = CI_TYPES.get(CI_TYPES_KEYS.V8).jobName;
export const CI_V8_URL = `https://${CI_DOMAIN}/job/${CI_V8_NAME}/build`;

export class RunPRJob {
  constructor(cli, request, owner, repo, prid, certifySafe, checkForDuplicates) {
    this.cli = cli;
    this.request = request;
    this.owner = owner;
    this.repo = repo;
    this.prid = prid;
    this.prData = new PRData({ prid, owner, repo }, cli, request);
    this.certifySafe =
      certifySafe ||
      Promise.all([this.prData.getReviews(), this.prData.getCommits()]).then(() =>
        (this.certifySafe = new PRChecker(cli, this.prData, request, {}).getApprovedTipOfHead())
      );
    this.checkForDuplicates = checkForDuplicates;
  }

  async getCrumb() {
    try {
      const { crumb } = await this.request.json(CI_CRUMB_URL);
      return crumb;
    } catch {
      return false;
    }
  }

  get payload() {
    const payload = new FormData();
    payload.append('json', JSON.stringify({
      parameter: [
        { name: 'CERTIFY_SAFE', value: 'on' },
        { name: 'COMMIT_SHA_CHECK', value: this.certifySafe },
        { name: 'TARGET_GITHUB_ORG', value: this.owner },
        { name: 'TARGET_REPO_NAME', value: this.repo },
        { name: 'PR_ID', value: this.prid },
        { name: 'REBASE_ONTO', value: '<pr base branch>' },
        { name: 'DESCRIPTION_SETTER_DESCRIPTION', value: '' }
      ]
    }));
    return payload;
  }

  get v8Payload() {
    const payload = new FormData();
    payload.append('json', JSON.stringify({
      parameter: [
        { name: 'GITHUB_ORG', value: this.owner },
        { name: 'REPO_NAME', value: this.repo },
        { name: 'GIT_REMOTE_REF', value: `refs/pull/${this.prid}/head` },
        { name: 'COMMIT_SHA_CHECK', value: this.certifySafe }
      ]
    }));
    return payload;
  }

  async start() {
    const { cli, request, checkForDuplicates } = this;
    const certifySafe = await this.certifySafe;

    if (!certifySafe) {
      cli.error('Refusing to run CI on potentially unsafe PR');
      return false;
    }

    cli.startSpinner('Validating Jenkins credentials');
    const crumb = await this.getCrumb();

    if (crumb === false) {
      cli.stopSpinner('Jenkins credentials invalid',
        this.cli.SPINNER_STATUS.FAILED);
      return false;
    }
    cli.stopSpinner('Jenkins credentials valid');

    if (checkForDuplicates) {
      await this.prData.getComments();
      const { jobid, link } = new JobParser(this.prData.comments).parse().get('PR') ?? {};
      let buildData;
      try {
        if (jobid) {
          buildData = await new PRBuild(cli, request, jobid, undefined, RESUME_TREE)
            .getBuildData();
        }
      } catch (err) {
        // Jenkins may no longer have the build (e.g. the record was lost) or
        // may answer with an HTML error page instead of JSON. Do not let that
        // block starting a new CI run.
        cli.stopSpinner(`Could not query existing CI run ${link}: ${err.message}`,
          cli.SPINNER_STATUS.WARN);
        cli.warn('Skipping the duplicate CI check');
      }
      const matchingSHA = buildData?.actions?.some(action =>
        action.parameters?.some(parameter =>
          parameter.name === 'COMMIT_SHA_CHECK' && parameter.value === certifySafe));
      if (matchingSHA) {
        cli.info('Existing CI run found: ' + link);
        let advice = 'Check the existing CI run in Jenkins before retrying.';
        if (getApprovedSHA(buildData) === certifySafe && buildData.building === false &&
            ['FAILURE', 'ABORTED'].includes(buildData.result)) {
          try {
            const { build } = await findResumableBuild(cli, request, {
              jobid,
              owner: this.owner,
              repo: this.repo,
              prid: this.prid
            }, buildData);
            advice = getCIActionAdvice(this, build ? 'resume' : 'run');
          } catch (err) {
            cli.error(`Could not check whether existing CI run ${link} can be resumed: ` +
              err.message);
            cli.error('Refusing to start a potentially duplicate CI job. ' +
              'Retry after checking Jenkins access.');
            return false;
          }
        } else if (buildData.building === true) {
          advice = 'The existing CI run is still in progress.';
        } else if (buildData.result === 'SUCCESS') {
          advice = 'CI has already succeeded for this commit.';
        }
        cli.error(`Refusing to start a potentially duplicate CI job. ${advice}`);
        return false;
      }
    }

    try {
      cli.startSpinner('Starting PR CI job');
      const response = await request.fetch(CI_PR_URL, {
        method: 'POST',
        headers: {
          'Jenkins-Crumb': crumb
        },
        body: this.payload
      });
      if (response.status !== 201) {
        cli.stopSpinner(
          `Failed to start PR CI: ${response.status} ${response.statusText}`,
          this.cli.SPINNER_STATUS.FAILED);
        return false;
      }
      cli.stopSpinner('PR CI job successfully started');

      // check if the job need a v8 build and trigger it
      await this.prData.getPR();
      const { labels } = this.prData.pr;
      if (labels?.nodes.some(i => i.name === 'v8 engine')) {
        cli.startSpinner('Starting V8 CI job');
        const response = await request.fetch(CI_V8_URL, {
          method: 'POST',
          headers: {
            'Jenkins-Crumb': crumb
          },
          body: this.v8Payload
        });
        if (response.status !== 201) {
          cli.stopSpinner(
            `Failed to start V8 CI: ${response.status} ${response.statusText}`,
            this.cli.SPINNER_STATUS.FAILED);
          return false;
        }
        cli.stopSpinner('V8 CI job successfully started');
      }
    } catch (err) {
      debuglog(err);
      cli.stopSpinner('Failed to start CI', this.cli.SPINNER_STATUS.FAILED);
      return false;
    }
    return true;
  }
}