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;
}
}