ui/models/job.js (220 lines of code) (raw):
import groupBy from 'lodash/groupBy';
import { createQueryParams, getApiUrl } from '../helpers/url';
import { formatTaskclusterError } from '../helpers/errorMessage';
import { addAggregateFields } from '../helpers/job';
import { getProjectUrl } from '../helpers/location';
import { getData } from '../helpers/http';
import { getAction } from '../helpers/taskcluster';
import PushModel from './push';
import TaskclusterModel from './taskcluster';
const uri = '/jobs/';
// JobModel is the js counterpart of job
export default class JobModel {
static async getList(options, config = {}) {
// The `uri` config allows to fetch a list of jobs from an arbitrary
// endpoint e.g. the similar jobs endpoint. It defaults to the job
// list endpoint.
const { fetchAll, uri: configUri } = config;
const jobUri = configUri || getApiUrl(uri);
const { data, failureStatus } = await getData(
`${jobUri}${options ? createQueryParams(options) : ''}`,
);
if (!failureStatus) {
const {
results,
job_property_names: jobPropertyNames,
next: nextUrl,
} = data;
let itemList;
let nextPagesJobs = [];
if (fetchAll && nextUrl) {
const page = new URLSearchParams(nextUrl.split('?')[1]).get('page');
const newOptions = { ...options, page };
const {
data: nextData,
failureStatus: nextFailureStatus,
} = await JobModel.getList(newOptions, config);
if (!nextFailureStatus) {
nextPagesJobs = nextData;
}
}
if (jobPropertyNames) {
// the results came as list of fields
// we need to convert them to objects
itemList = results.map((elem) =>
addAggregateFields(
jobPropertyNames.reduce(
(prev, prop, i) => ({ ...prev, [prop]: elem[i] }),
{},
),
),
);
} else {
itemList = results.map((jobObj) => addAggregateFields(jobObj));
}
return { data: [...itemList, ...nextPagesJobs], failureStatus: null };
}
return { data, failureStatus };
}
static get(repoName, pk, signal) {
// a static method to retrieve a single instance of JobModel
return fetch(`${getProjectUrl(uri, repoName)}${pk}/`, { signal }).then(
async (response) => {
if (response.ok) {
const job = await response.json();
return addAggregateFields(job);
}
const text = await response.text();
throw Error(`Loading job with id ${pk} : ${text}`);
},
);
}
static getSimilarJobs(pk, options, config) {
config = config || {};
// The similar jobs endpoints returns the same type of objects as
// the job list endpoint, so let's reuse the getList method logic.
config.uri = `${getProjectUrl(uri)}${pk}/similar_jobs/`;
return JobModel.getList(options, config);
}
static async retrigger(
jobs,
currentRepo,
notify,
times = 1,
decisionTaskIdMap = null,
testMode = false,
) {
const jobTerm = jobs.length > 1 ? 'jobs' : 'job';
try {
notify(`Attempting to retrigger/add ${jobTerm} via actions.json`);
const pushIds = [...new Set(jobs.map((job) => job.push_id))];
const taskIdMap =
decisionTaskIdMap ||
(await PushModel.getDecisionTaskMap(pushIds, notify));
const uniquePerPushJobs = groupBy(jobs, (job) => job.push_id);
for (const [key, value] of Object.entries(uniquePerPushJobs)) {
const decisionTaskId = taskIdMap[key].id;
TaskclusterModel.load(decisionTaskId, null, currentRepo, testMode)
.then(async (results) => {
const taskLabels = value.map((job) => job.job_type_name);
let retriggerAction = results.actions.find(
(action) => action.name === 'retrigger-multiple',
);
let actionInput = {
requests: [{ tasks: taskLabels, times }],
};
if (!retriggerAction) {
// The `retrigger-multiple` action as introduced in Bug 1521032, to all the action
// to control whether new task are created, or existing ones re-run. We fall back
// to `add-new-jobs` to support pushing old revision to try, where the duplicating
// the release tasks impacted is unlikely to cause problems.
retriggerAction = getAction(results.actions, 'add-new-jobs');
actionInput = {
tasks: taskLabels,
};
}
await TaskclusterModel.submit({
action: retriggerAction,
decisionTaskId,
taskId: null,
task: null,
input: actionInput,
staticActionVariables: results.staticActionVariables,
currentRepo,
testMode,
})
.then((actionTaskId) =>
notify(
`Request sent to retrigger/add new jobs via actions.json (${actionTaskId})`,
),
)
.catch((error) => {
notify(
`Retrigger failed with Decision task: ${decisionTaskId}: ${error}`,
'danger',
{ sticky: true },
);
});
})
.catch((error) => notify(error.message, 'danger', { sticky: true }));
}
} catch (e) {
notify(
`Unable to retrigger/add ${jobTerm}. ${formatTaskclusterError(e)}`,
'danger',
{ sticky: true },
);
}
}
static async cancelAll(
pushId,
currentRepo,
notify,
decisionTask,
testMode = false,
) {
const { id: decisionTaskId } =
decisionTask || (await PushModel.getDecisionTaskId(pushId, notify));
let results;
try {
results = await TaskclusterModel.load(
decisionTaskId,
null,
currentRepo,
testMode,
);
} catch (e) {
notify(e.message, 'danger', { sticky: true });
}
try {
const cancelAllTask = getAction(results.actions, 'cancel-all');
await TaskclusterModel.submit({
action: cancelAllTask,
decisionTaskId,
input: {},
staticActionVariables: results.staticActionVariables,
currentRepo,
testMode,
});
} catch (e) {
// The full message is too large to fit in a Treeherder
// notification box.
notify(formatTaskclusterError(e), 'danger', { sticky: true });
}
notify('Request sent to cancel all jobs via action.json', 'success');
}
static async cancel(
jobs,
currentRepo,
notify,
decisionTaskIdMap = null,
testMode = false,
) {
const jobTerm = jobs.length > 1 ? 'jobs' : 'job';
const taskIdMap =
decisionTaskIdMap ||
(await PushModel.getDecisionTaskMap(
[...new Set(jobs.map((job) => job.push_id))],
notify,
));
try {
notify(`Attempting to cancel selected ${jobTerm} via actions.json`);
/* eslint-disable no-await-in-loop */
for (const job of jobs) {
const decisionTaskId = taskIdMap[job.push_id].id;
let results;
try {
results = await TaskclusterModel.load(
decisionTaskId,
job,
currentRepo,
testMode,
);
} catch (e) {
notify(e.message, 'danger', { sticky: true });
}
try {
const cancelTask = getAction(results.actions, 'cancel');
await TaskclusterModel.submit({
action: cancelTask,
decisionTaskId,
taskId: results.originalTaskId,
input: {},
staticActionVariables: results.staticActionVariables,
currentRepo,
testMode,
});
} catch (e) {
// The full message is too large to fit in a Treeherder
// notification box.
notify(formatTaskclusterError(e), 'danger', { sticky: true });
}
}
/* eslint-enable no-await-in-loop */
notify(`Request sent to cancel ${jobTerm} via action.json`, 'success');
} catch {
notify(`Unable to cancel ${jobTerm}`, 'danger', { sticky: true });
}
}
}