// vim: set noai ts=2 sw=2 et: */ // This is a read-only Buildkite token: it was generated by lukegb@, and has // read_builds, read_build_logs, and read_pipelines permissions. const BUILDKITE_TOKEN = 'a150658fb61062e432f13a032962d70fa9352088'; function encodeParams(p) { const pieces = []; for (let k of Object.getOwnPropertyNames(p)) { pieces.push(`${encodeURIComponent(k)}=${encodeURIComponent(p[k])}`); } return pieces.join('&'); } function formatDuration(from, to) { const millisecondsTook = Math.floor(to.valueOf() - from.valueOf()); if (millisecondsTook < 2000) return `${millisecondsTook} ms`; const secondsTook = Math.floor(millisecondsTook / 1000); if (secondsTook < 100) return `${secondsTook} seconds`; const minutesTook = Math.floor(secondsTook / 60); if (minutesTook < 60) return `${minutesTook} minutes`; const hoursTook = Math.floor(minutesTook / 60); const minutesRemainder = minutesTook - (hoursTook * 60); return `${hoursTook}hr ${minutesRemainder}min`; } // Maps the status of a Buildkite *job* to the statuses available for // a Gerrit check. // // Note that jobs can have statuses that, according to the Buildkite // documentation, are only available for builds, and maybe vice-versa. // To deal with this we simply cover all statuses for all types here. // // Buildkite job statuses: https://buildkite.com/docs/pipelines/notifications#job-states // // Gerrit check statuses: https://gerrit.googlesource.com/gerrit/+/v3.4.0/polygerrit-ui/app/api/checks.ts#167 // // TODO(tazjin): Use SCHEDULED status once we have upgraded Gerrit // past 3.4 function jobStateToCheckRunStatus(state) { const status = { // Statuses documented for both types 'blocked': 'RUNNABLE', 'canceled': 'COMPLETED', 'canceling': 'RUNNING', 'running': 'RUNNING', 'scheduled': 'RUNNABLE', 'skipped': 'COMPLETED', // Statuses only documented for builds 'creating': 'RUNNABLE', 'failed': 'COMPLETED', 'not_run': 'COMPLETED', 'passed': 'COMPLETED', // Statuses only documented for jobs 'accepted': 'RUNNABLE', 'assigned': 'RUNNABLE', 'blocked_failed': 'COMPLETED', 'broken': 'COMPLETED', 'finished': 'COMPLETED', 'limited': 'RUNNABLE', 'limiting': 'RUNNABLE', 'pending': 'RUNNABLE', 'timed_out': 'COMPLETED', 'timing_out': 'RUNNING', 'unblocked': 'RUNNABLE', 'unblocked_failed': 'COMPLETED', 'waiting': 'RUNNABLE', 'waiting_failed': 'COMPLETED', }[state]; if (!status) { console.log(`unknown Buildkite job state: ${state}`); } return status; } const tvlChecksProvider = { async fetch(change) { let {changeNumber, patchsetSha, repo} = change; const experiments = window.ENABLED_EXPERIMENTS || []; if (experiments.includes("UiFeature__tvl_check_debug")) { changeNumber = 2872; patchsetSha = '76692104f58b849b1503a8d8a700298003fa7b5f'; repo = 'depot'; } if (repo !== 'depot') { // We only handle TVL's depot at the moment. return {responseCode: 'OK'}; } const params = { // besadii groups different patchsets of the same CL under this fake ref branch: `cl/${changeNumber.toString()}`, commit: patchsetSha, }; const url = `https://api.buildkite.com/v2/organizations/tvl/pipelines/depot/builds?${encodeParams(params)}`; const resp = await fetch(url, { headers: { Authorization: `Bearer ${BUILDKITE_TOKEN}`, }, }); const respJSON = await resp.json(); const runs = []; for (let i = 0; i < respJSON.length; i++) { const attempt = respJSON.length - i; const build = respJSON[i]; for (let job of build.jobs) { // Skip non-command jobs (e.g. waiting/grouping jobs) if (job.type !== 'script') { continue; } // Skip jobs marked as 'broken' (this means they were skipped // intentionally) if (job.state === 'broken') { continue; } // TODO(lukegb): add the ability to retry these const checkRun = { patchset: parseInt(build.env.GERRIT_PATCHSET, 10), attempt: attempt, externalId: job.id, checkName: job.name, checkDescription: job.command, checkLink: job.web_url, status: jobStateToCheckRunStatus(job.state), labelName: 'Verified', }; if (job.scheduled_at) { checkRun.scheduledTimestamp = new Date(job.scheduled_at); } if (job.started_at) { checkRun.startedTimestamp = new Date(job.started_at); } if (job.finished_at) { checkRun.finishedTimestamp = new Date(job.finished_at); } let statusDescription = job.state; if (checkRun.startedTimestamp && checkRun.finishedTimestamp) { statusDescription = `${statusDescription} in ${formatDuration(checkRun.startedTimestamp, checkRun.finishedTimestamp)}`; } else if (checkRun.startedTimestamp) { statusDescription = `${statusDescription} for ${formatDuration(checkRun.startedTimestamp, new Date())}`; } else if (checkRun.scheduledTimestamp) { statusDescription = `${statusDescription} for ${formatDuration(checkRun.scheduledTimestamp, new Date())}`; } checkRun.statusDescription = statusDescription; if (['failed', 'timed_out'].includes(job.state)) { const result = { // TODO(lukegb): get the log as the message here (the Gerrit // implementation doesn't yet seem to support newlines in message // strings...) links: [{ url: job.web_url, tooltip: "Buildkite", primary: true, icon: 'EXTERNAL', }], category: 'ERROR', summary: `${job.command} failed`, }; checkRun.results = [result]; } runs.push(checkRun); } } return { responseCode: 'OK', runs: runs, }; }, }; Gerrit.install(plugin => { console.log('TVL plugin initialising'); plugin.checks().register(tvlChecksProvider); });