зеркало из https://github.com/mozilla/treeherder.git
Bug 1566077 - Improve getting decision task ID (#5360)
This commit is contained in:
Родитель
b462c6be5e
Коммит
aff331f3d3
|
@ -1,9 +1,11 @@
|
|||
import { fetchMock } from 'fetch-mock';
|
||||
|
||||
import JobModel from '../../../ui/models/job';
|
||||
import { decisionTaskIdCache } from '../../../ui/models/push';
|
||||
import { getApiUrl } from '../../../ui/helpers/url';
|
||||
import paginatedJobListFixtureOne from '../mock/job_list/pagination/page_1';
|
||||
import paginatedJobListFixtureTwo from '../mock/job_list/pagination/page_2';
|
||||
import { getProjectUrl } from '../../../ui/helpers/location';
|
||||
|
||||
describe('JobModel', () => {
|
||||
afterEach(() => {
|
||||
|
@ -40,12 +42,40 @@ describe('JobModel', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('retriggering ', () => {
|
||||
describe('Taskcluster actions', () => {
|
||||
const decisionTaskMap = {
|
||||
'526443': { id: 'LVTawdmFR2-uJiWWS2NxSw', run: '0' },
|
||||
};
|
||||
const tcActionsUrl =
|
||||
'https://queue.taskcluster.net/v1/task/LVTawdmFR2-uJiWWS2NxSw/artifacts/public%2Factions.json';
|
||||
const tcTaskUrl = 'https://queue.taskcluster.net/v1/task/TASKID';
|
||||
const decisionTaskMapUrl = getProjectUrl(
|
||||
'/push/decisiontask/?push_ids=526443',
|
||||
'autoland',
|
||||
);
|
||||
const notify = () => {};
|
||||
const testJobs = [
|
||||
{ id: 123, push_id: 526443, job_type_name: 'foo', task_id: 'TASKID' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mock(
|
||||
getApiUrl('/jobs/?push_id=526443'),
|
||||
paginatedJobListFixtureOne,
|
||||
);
|
||||
fetchMock.mock(
|
||||
getApiUrl('/taskclustermetadata/?job_ids=123'),
|
||||
paginatedJobListFixtureOne,
|
||||
);
|
||||
fetchMock.mock(decisionTaskMapUrl, decisionTaskMap);
|
||||
fetchMock.get(tcActionsUrl, { version: 1, actions: [{ name: 'foo' }] });
|
||||
fetchMock.get(tcTaskUrl, {});
|
||||
|
||||
// Must clear the cache, because we save each time we
|
||||
// call the API for a decision task id.
|
||||
Object.keys(decisionTaskIdCache).forEach(
|
||||
prop => delete decisionTaskIdCache[prop],
|
||||
);
|
||||
});
|
||||
|
||||
test('jobs should have required fields', async () => {
|
||||
|
@ -55,5 +85,61 @@ describe('JobModel', () => {
|
|||
expect(signature).toBe('2aa083621bb989d6acf1151667288d5fe9616178');
|
||||
expect(job_type_name).toBe('Gecko Decision Task');
|
||||
});
|
||||
|
||||
test('retrigger uses passed-in decisionTaskMap', async () => {
|
||||
await JobModel.retrigger(
|
||||
testJobs,
|
||||
'autoland',
|
||||
notify,
|
||||
1,
|
||||
decisionTaskMap,
|
||||
);
|
||||
|
||||
expect(fetchMock.called(decisionTaskMapUrl)).toBe(false);
|
||||
expect(fetchMock.called(tcTaskUrl)).toBe(false);
|
||||
expect(fetchMock.called(tcActionsUrl)).toBe(true);
|
||||
});
|
||||
|
||||
test('retrigger calls for decision task when not passed-in', async () => {
|
||||
await JobModel.retrigger(testJobs, 'autoland', notify, 1);
|
||||
|
||||
expect(fetchMock.called(decisionTaskMapUrl)).toBe(true);
|
||||
expect(fetchMock.called(tcTaskUrl)).toBe(false);
|
||||
expect(fetchMock.called(tcActionsUrl)).toBe(true);
|
||||
});
|
||||
|
||||
test('cancel uses passed-in decisionTask', async () => {
|
||||
await JobModel.cancel(testJobs, 'autoland', () => {}, decisionTaskMap);
|
||||
|
||||
expect(fetchMock.called(decisionTaskMapUrl)).toBe(false);
|
||||
expect(fetchMock.called(tcTaskUrl)).toBe(true);
|
||||
expect(fetchMock.called(tcActionsUrl)).toBe(true);
|
||||
});
|
||||
|
||||
test('cancel calls for decision task when not passed-in', async () => {
|
||||
await JobModel.cancel(testJobs, 'autoland', () => {});
|
||||
|
||||
expect(fetchMock.called(decisionTaskMapUrl)).toBe(true);
|
||||
expect(fetchMock.called(tcTaskUrl)).toBe(true);
|
||||
expect(fetchMock.called(tcActionsUrl)).toBe(true);
|
||||
});
|
||||
|
||||
test('cancelAll uses passed-in decisionTask', async () => {
|
||||
const decisionTask = { id: 'LVTawdmFR2-uJiWWS2NxSw', run: '0' };
|
||||
|
||||
await JobModel.cancelAll(526443, 'autoland', () => {}, decisionTask);
|
||||
|
||||
expect(fetchMock.called(decisionTaskMapUrl)).toBe(false);
|
||||
expect(fetchMock.called(tcTaskUrl)).toBe(false);
|
||||
expect(fetchMock.called(tcActionsUrl)).toBe(true);
|
||||
});
|
||||
|
||||
test('cancelAll calls for decision task when not passed-in', async () => {
|
||||
await JobModel.cancelAll(526443, 'autoland', () => {});
|
||||
|
||||
expect(fetchMock.called(decisionTaskMapUrl)).toBe(true);
|
||||
expect(fetchMock.called(tcTaskUrl)).toBe(false);
|
||||
expect(fetchMock.called(tcActionsUrl)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,11 +9,14 @@ describe('PushModel', () => {
|
|||
});
|
||||
|
||||
describe('taskcluster actions', () => {
|
||||
const decisionTaskUrl = getProjectUrl(
|
||||
'/push/decisiontask/?push_ids=548880',
|
||||
'autoland',
|
||||
);
|
||||
beforeEach(() => {
|
||||
fetchMock.mock(
|
||||
getProjectUrl('/push/decisiontask/?push_ids=548880', 'autoland'),
|
||||
{ '548880': { id: 'U-lI3jzPTkWFplfJPz6cJA', run: '0' } },
|
||||
);
|
||||
fetchMock.mock(decisionTaskUrl, {
|
||||
'548880': { id: 'U-lI3jzPTkWFplfJPz6cJA', run: '0' },
|
||||
});
|
||||
});
|
||||
|
||||
test('getDecisionTaskId', async () => {
|
||||
|
@ -26,6 +29,11 @@ describe('PushModel', () => {
|
|||
id: 'U-lI3jzPTkWFplfJPz6cJA',
|
||||
run: '0',
|
||||
});
|
||||
expect(fetchMock.calls(decisionTaskUrl)).toHaveLength(1);
|
||||
|
||||
await PushModel.getDecisionTaskId(548880, () => {});
|
||||
// on second try, it was cached. So we still have just 1 call
|
||||
expect(fetchMock.calls(decisionTaskUrl)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('getDecisionTaskMap', async () => {
|
||||
|
|
|
@ -108,6 +108,7 @@ class JobsViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
'job_group',
|
||||
'machine_platform',
|
||||
'signature',
|
||||
'taskcluster_metadata',
|
||||
]
|
||||
_query_field_names = [
|
||||
'submit_time',
|
||||
|
@ -128,6 +129,9 @@ class JobsViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
'signature__signature',
|
||||
'state',
|
||||
'tier',
|
||||
'taskcluster_metadata__task_id',
|
||||
'taskcluster_metadata__retry_id',
|
||||
|
||||
]
|
||||
_output_field_names = [
|
||||
'failure_classification_id',
|
||||
|
@ -143,6 +147,8 @@ class JobsViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
'signature',
|
||||
'state',
|
||||
'tier',
|
||||
'task_id',
|
||||
'retry_id',
|
||||
'duration',
|
||||
'platform_option',
|
||||
]
|
||||
|
@ -177,7 +183,7 @@ class JobsProjectViewSet(viewsets.ViewSet):
|
|||
'machine_platform',
|
||||
'machine',
|
||||
'signature',
|
||||
'repository'
|
||||
'repository',
|
||||
]
|
||||
|
||||
_property_query_mapping = [
|
||||
|
@ -289,6 +295,9 @@ class JobsProjectViewSet(viewsets.ViewSet):
|
|||
resp["platform_option"] = platform_option
|
||||
|
||||
try:
|
||||
resp['task_id'] = job.taskcluster_metadata.task_id
|
||||
resp['retry_id'] = job.taskcluster_metadata.retry_id
|
||||
# Keep for backwards compatability
|
||||
resp['taskcluster_metadata'] = {
|
||||
'task_id': job.taskcluster_metadata.task_id,
|
||||
'retry_id': job.taskcluster_metadata.retry_id
|
||||
|
|
|
@ -22,7 +22,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import { faCheckSquare } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
import { formatTaskclusterError } from '../helpers/errorMessage';
|
||||
import PushModel from '../models/push';
|
||||
import TaskclusterModel from '../models/taskcluster';
|
||||
|
||||
import { notify } from './redux/stores/notifications';
|
||||
|
@ -46,11 +45,8 @@ class CustomJobActions extends React.PureComponent {
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { pushId, job, notify } = this.props;
|
||||
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
|
||||
pushId,
|
||||
notify,
|
||||
);
|
||||
const { pushId, job, notify, decisionTaskMap } = this.props;
|
||||
const { id: decisionTaskId } = decisionTaskMap[pushId];
|
||||
|
||||
TaskclusterModel.load(decisionTaskId, job).then(results => {
|
||||
const {
|
||||
|
@ -308,6 +304,7 @@ CustomJobActions.propTypes = {
|
|||
isLoggedIn: PropTypes.bool.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
decisionTaskMap: PropTypes.object.isRequired,
|
||||
job: PropTypes.object,
|
||||
};
|
||||
|
||||
|
@ -315,7 +312,11 @@ CustomJobActions.defaultProps = {
|
|||
job: null,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({
|
||||
decisionTaskMap,
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapStateToProps,
|
||||
{ notify },
|
||||
)(CustomJobActions);
|
||||
|
|
|
@ -211,8 +211,7 @@ class DetailsPanel extends React.Component {
|
|||
phSeriesResult,
|
||||
]) => {
|
||||
// This version of the job has more information than what we get in the main job list. This
|
||||
// is what we'll pass to the rest of the details panel. It has extra fields like
|
||||
// taskcluster_metadata.
|
||||
// is what we'll pass to the rest of the details panel.
|
||||
// Don't update the job instance in the greater job field so as to not add the memory overhead
|
||||
// of all the extra fields in ``selectedJobFull``. It's not that much for just one job, but as
|
||||
// one selects job after job, over the course of a day, it can add up. Therefore, we keep
|
||||
|
|
|
@ -198,12 +198,17 @@ class PinBoard extends React.Component {
|
|||
};
|
||||
|
||||
cancelAllPinnedJobs = () => {
|
||||
const { notify, repoName, pinnedJobs } = this.props;
|
||||
const { notify, repoName, pinnedJobs, decisionTaskMap } = this.props;
|
||||
|
||||
if (
|
||||
window.confirm('This will cancel all the selected jobs. Are you sure?')
|
||||
) {
|
||||
JobModel.cancel(Object.values(pinnedJobs), repoName, notify);
|
||||
JobModel.cancel(
|
||||
Object.values(pinnedJobs),
|
||||
repoName,
|
||||
notify,
|
||||
decisionTaskMap,
|
||||
);
|
||||
this.unPinAll();
|
||||
}
|
||||
};
|
||||
|
@ -348,10 +353,11 @@ class PinBoard extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
retriggerAllPinnedJobs = () => {
|
||||
const { pinnedJobs, notify, repoName } = this.props;
|
||||
retriggerAllPinnedJobs = async () => {
|
||||
const { pinnedJobs, notify, repoName, decisionTaskMap } = this.props;
|
||||
const jobs = Object.values(pinnedJobs);
|
||||
|
||||
JobModel.retrigger(Object.values(pinnedJobs), repoName, notify);
|
||||
JobModel.retrigger(jobs, repoName, notify, 1, decisionTaskMap);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -630,6 +636,7 @@ class PinBoard extends React.Component {
|
|||
|
||||
PinBoard.propTypes = {
|
||||
recalculateUnclassifiedCounts: PropTypes.func.isRequired,
|
||||
decisionTaskMap: PropTypes.object.isRequired,
|
||||
classificationTypes: PropTypes.array.isRequired,
|
||||
isLoggedIn: PropTypes.bool.isRequired,
|
||||
isPinBoardVisible: PropTypes.bool.isRequired,
|
||||
|
@ -659,7 +666,7 @@ PinBoard.defaultProps = {
|
|||
};
|
||||
|
||||
const mapStateToProps = ({
|
||||
pushes: { revisionTips },
|
||||
pushes: { revisionTips, decisionTaskMap },
|
||||
pinnedJobs: {
|
||||
isPinBoardVisible,
|
||||
pinnedJobs,
|
||||
|
@ -669,6 +676,7 @@ const mapStateToProps = ({
|
|||
},
|
||||
}) => ({
|
||||
revisionTips,
|
||||
decisionTaskMap,
|
||||
isPinBoardVisible,
|
||||
pinnedJobs,
|
||||
pinnedJobBugs,
|
||||
|
|
|
@ -16,7 +16,6 @@ import { formatTaskclusterError } from '../../../helpers/errorMessage';
|
|||
import { isReftest, isPerfTest, isTestIsolatable } from '../../../helpers/job';
|
||||
import { getInspectTaskUrl, getReftestUrl } from '../../../helpers/url';
|
||||
import JobModel from '../../../models/job';
|
||||
import PushModel from '../../../models/push';
|
||||
import TaskclusterModel from '../../../models/taskcluster';
|
||||
import CustomJobActions from '../../CustomJobActions';
|
||||
import { notify } from '../../redux/stores/notifications';
|
||||
|
@ -78,15 +77,12 @@ class ActionBar extends React.PureComponent {
|
|||
};
|
||||
|
||||
createGeckoProfile = async () => {
|
||||
const { user, selectedJobFull, notify } = this.props;
|
||||
const { user, selectedJobFull, notify, decisionTaskMap } = this.props;
|
||||
if (!user.isLoggedIn) {
|
||||
return notify('Must be logged in to create a gecko profile', 'danger');
|
||||
}
|
||||
|
||||
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
|
||||
selectedJobFull.push_id,
|
||||
notify,
|
||||
);
|
||||
const decisionTaskId = decisionTaskMap[selectedJobFull.push_id];
|
||||
TaskclusterModel.load(decisionTaskId, selectedJobFull).then(results => {
|
||||
try {
|
||||
const geckoprofile = getAction(results.actions, 'geckoprofile');
|
||||
|
@ -126,8 +122,8 @@ class ActionBar extends React.PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
retriggerJob = jobs => {
|
||||
const { user, repoName, notify } = this.props;
|
||||
retriggerJob = async jobs => {
|
||||
const { user, repoName, notify, decisionTaskMap } = this.props;
|
||||
|
||||
if (!user.isLoggedIn) {
|
||||
return notify('Must be logged in to retrigger a job', 'danger');
|
||||
|
@ -145,11 +141,11 @@ class ActionBar extends React.PureComponent {
|
|||
});
|
||||
});
|
||||
|
||||
JobModel.retrigger(jobs, repoName, notify);
|
||||
JobModel.retrigger(jobs, repoName, notify, 1, decisionTaskMap);
|
||||
};
|
||||
|
||||
backfillJob = async () => {
|
||||
const { user, selectedJobFull, notify } = this.props;
|
||||
const { user, selectedJobFull, notify, decisionTaskMap } = this.props;
|
||||
|
||||
if (!this.canBackfill()) {
|
||||
return;
|
||||
|
@ -167,10 +163,8 @@ class ActionBar extends React.PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
|
||||
selectedJobFull.push_id,
|
||||
notify,
|
||||
);
|
||||
const { id: decisionTaskId } = decisionTaskMap[selectedJobFull.push_id];
|
||||
|
||||
TaskclusterModel.load(decisionTaskId, selectedJobFull).then(results => {
|
||||
try {
|
||||
const backfilltask = getAction(results.actions, 'backfill');
|
||||
|
@ -198,11 +192,8 @@ class ActionBar extends React.PureComponent {
|
|||
};
|
||||
|
||||
isolateJob = async () => {
|
||||
const { user, selectedJobFull, notify } = this.props;
|
||||
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
|
||||
selectedJobFull.push_id,
|
||||
notify,
|
||||
);
|
||||
const { user, selectedJobFull, notify, decisionTaskMap } = this.props;
|
||||
const { id: decisionTaskId } = decisionTaskMap[selectedJobFull.push_id];
|
||||
|
||||
if (!isTestIsolatable(selectedJobFull)) {
|
||||
return;
|
||||
|
@ -317,8 +308,7 @@ class ActionBar extends React.PureComponent {
|
|||
};
|
||||
|
||||
createInteractiveTask = async () => {
|
||||
const { user, selectedJobFull, repoName, notify } = this.props;
|
||||
const jobId = selectedJobFull.id;
|
||||
const { user, selectedJobFull, notify, decisionTaskMap } = this.props;
|
||||
|
||||
if (!user.isLoggedIn) {
|
||||
return notify(
|
||||
|
@ -327,12 +317,11 @@ class ActionBar extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const job = await JobModel.get(repoName, jobId);
|
||||
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
|
||||
job.push_id,
|
||||
notify,
|
||||
const { id: decisionTaskId } = decisionTaskMap[selectedJobFull.push_id];
|
||||
const results = await TaskclusterModel.load(
|
||||
decisionTaskId,
|
||||
selectedJobFull,
|
||||
);
|
||||
const results = await TaskclusterModel.load(decisionTaskId, job);
|
||||
|
||||
try {
|
||||
const interactiveTask = getAction(results.actions, 'create-interactive');
|
||||
|
@ -360,7 +349,7 @@ class ActionBar extends React.PureComponent {
|
|||
};
|
||||
|
||||
cancelJobs = jobs => {
|
||||
const { user, repoName, notify } = this.props;
|
||||
const { user, repoName, notify, decisionTaskMap } = this.props;
|
||||
|
||||
if (!user.isLoggedIn) {
|
||||
return notify('Must be logged in to cancel a job', 'danger');
|
||||
|
@ -369,6 +358,7 @@ class ActionBar extends React.PureComponent {
|
|||
jobs.filter(({ state }) => state === 'pending' || state === 'running'),
|
||||
repoName,
|
||||
notify,
|
||||
decisionTaskMap,
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -490,16 +480,14 @@ class ActionBar extends React.PureComponent {
|
|||
Backfill
|
||||
</span>
|
||||
</li>
|
||||
{selectedJobFull.taskcluster_metadata && (
|
||||
{selectedJobFull.task_id && (
|
||||
<React.Fragment>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="dropdown-item pl-4"
|
||||
href={getInspectTaskUrl(
|
||||
selectedJobFull.taskcluster_metadata.task_id,
|
||||
)}
|
||||
href={getInspectTaskUrl(selectedJobFull.task_id)}
|
||||
>
|
||||
Inspect Task
|
||||
</a>
|
||||
|
@ -561,6 +549,7 @@ class ActionBar extends React.PureComponent {
|
|||
|
||||
ActionBar.propTypes = {
|
||||
pinJob: PropTypes.func.isRequired,
|
||||
decisionTaskMap: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
repoName: PropTypes.string.isRequired,
|
||||
selectedJobFull: PropTypes.object.isRequired,
|
||||
|
@ -579,7 +568,11 @@ ActionBar.defaultProps = {
|
|||
jobLogUrls: [],
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({
|
||||
decisionTaskMap,
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapStateToProps,
|
||||
{ notify, pinJob },
|
||||
)(ActionBar);
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
import { getGroupMapKey } from '../../helpers/aggregateId';
|
||||
import { getAllUrlParams, getUrlParam } from '../../helpers/location';
|
||||
import JobModel from '../../models/job';
|
||||
import PushModel from '../../models/push';
|
||||
import RunnableJobModel from '../../models/runnableJob';
|
||||
import { getRevisionTitle } from '../../helpers/revision';
|
||||
import { getPercentComplete } from '../../helpers/display';
|
||||
|
@ -317,12 +316,11 @@ class Push extends React.PureComponent {
|
|||
};
|
||||
|
||||
showRunnableJobs = async () => {
|
||||
const { push, repoName, notify } = this.props;
|
||||
const { push, repoName, notify, decisionTaskMap } = this.props;
|
||||
|
||||
try {
|
||||
const decisionTaskId = await PushModel.getDecisionTaskId(push.id, notify);
|
||||
const jobList = await RunnableJobModel.getList(repoName, {
|
||||
decision_task_id: decisionTaskId,
|
||||
decisionTask: decisionTaskMap[push.id],
|
||||
push_id: push.id,
|
||||
});
|
||||
|
||||
|
@ -354,7 +352,7 @@ class Push extends React.PureComponent {
|
|||
};
|
||||
|
||||
showFuzzyJobs = async () => {
|
||||
const { push, repoName, notify } = this.props;
|
||||
const { push, repoName, notify, decisionTaskMap } = this.props;
|
||||
const createRegExp = (str, opts) =>
|
||||
new RegExp(str.raw[0].replace(/\s/gm, ''), opts || '');
|
||||
const excludedJobNames = createRegExp`
|
||||
|
@ -367,11 +365,9 @@ class Push extends React.PureComponent {
|
|||
test-verify|test-windows10-64-ux|toolchain|upload-generated-sources)`;
|
||||
|
||||
try {
|
||||
const decisionTaskId = await PushModel.getDecisionTaskId(push.id, notify);
|
||||
|
||||
notify('Fetching runnable jobs... This could take a while...');
|
||||
let fuzzyJobList = await RunnableJobModel.getList(repoName, {
|
||||
decision_task_id: decisionTaskId,
|
||||
decisionTask: decisionTaskMap[push.id],
|
||||
});
|
||||
fuzzyJobList = [
|
||||
...new Set(
|
||||
|
@ -390,7 +386,6 @@ class Push extends React.PureComponent {
|
|||
this.setState({
|
||||
fuzzyJobList,
|
||||
filteredFuzzyList,
|
||||
decisionTaskId: decisionTaskId.id,
|
||||
});
|
||||
this.toggleFuzzyModal();
|
||||
} catch (error) {
|
||||
|
@ -443,12 +438,12 @@ class Push extends React.PureComponent {
|
|||
groupCountsExpanded,
|
||||
isOnlyRevision,
|
||||
pushHealthVisibility,
|
||||
decisionTaskMap,
|
||||
} = this.props;
|
||||
const {
|
||||
fuzzyJobList,
|
||||
fuzzyModal,
|
||||
filteredFuzzyList,
|
||||
decisionTaskId,
|
||||
watched,
|
||||
runnableVisible,
|
||||
pushGroupState,
|
||||
|
@ -459,6 +454,8 @@ class Push extends React.PureComponent {
|
|||
} = this.state;
|
||||
const { id, push_timestamp, revision, author } = push;
|
||||
const tipRevision = push.revisions[0];
|
||||
const decisionTask = decisionTaskMap[push.id];
|
||||
const decisionTaskId = decisionTask ? decisionTask.id : null;
|
||||
|
||||
if (isOnlyRevision) {
|
||||
this.setSingleRevisionWindowTitle();
|
||||
|
@ -554,10 +551,14 @@ Push.propTypes = {
|
|||
notify: PropTypes.func.isRequired,
|
||||
isOnlyRevision: PropTypes.bool.isRequired,
|
||||
pushHealthVisibility: PropTypes.string.isRequired,
|
||||
decisionTaskMap: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ pushes: { allUnclassifiedFailureCount } }) => ({
|
||||
const mapStateToProps = ({
|
||||
pushes: { allUnclassifiedFailureCount, decisionTaskMap },
|
||||
}) => ({
|
||||
allUnclassifiedFailureCount,
|
||||
decisionTaskMap,
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -142,6 +142,7 @@ class PushHeader extends React.Component {
|
|||
selectedRunnableJobs,
|
||||
hideRunnableJobs,
|
||||
notify,
|
||||
decisionTaskMap,
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
|
@ -152,10 +153,7 @@ class PushHeader extends React.Component {
|
|||
return;
|
||||
}
|
||||
if (isLoggedIn) {
|
||||
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
|
||||
pushId,
|
||||
notify,
|
||||
);
|
||||
const { id: decisionTaskId } = decisionTaskMap[pushId];
|
||||
|
||||
PushModel.triggerNewJobs(selectedRunnableJobs, decisionTaskId)
|
||||
.then(result => {
|
||||
|
@ -172,18 +170,22 @@ class PushHeader extends React.Component {
|
|||
};
|
||||
|
||||
cancelAllJobs = () => {
|
||||
const { notify, repoName } = this.props;
|
||||
|
||||
if (
|
||||
window.confirm(
|
||||
'This will cancel all pending and running jobs for this push. It cannot be undone! Are you sure?',
|
||||
)
|
||||
) {
|
||||
const { push, isLoggedIn } = this.props;
|
||||
const {
|
||||
notify,
|
||||
repoName,
|
||||
push,
|
||||
isLoggedIn,
|
||||
decisionTaskMap,
|
||||
} = this.props;
|
||||
|
||||
if (!isLoggedIn) return;
|
||||
|
||||
JobModel.cancelAll(push.id, repoName, notify);
|
||||
JobModel.cancelAll(push.id, repoName, notify, decisionTaskMap[push.id]);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -407,6 +409,7 @@ PushHeader.propTypes = {
|
|||
notify: PropTypes.func.isRequired,
|
||||
jobCounts: PropTypes.object.isRequired,
|
||||
pushHealthVisibility: PropTypes.string.isRequired,
|
||||
decisionTaskMap: PropTypes.object.isRequired,
|
||||
watchState: PropTypes.string,
|
||||
};
|
||||
|
||||
|
@ -414,7 +417,11 @@ PushHeader.defaultProps = {
|
|||
watchState: 'none',
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({
|
||||
decisionTaskMap,
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapStateToProps,
|
||||
{ notify, setSelectedJob, pinJobs },
|
||||
)(PushHeader);
|
||||
|
|
|
@ -168,11 +168,28 @@ const fetchNewJobs = () => {
|
|||
};
|
||||
};
|
||||
|
||||
const doUpdateJobMap = (jobList, jobMap, pushList) => {
|
||||
const doUpdateJobMap = (jobList, jobMap, decisionTaskMap, pushList) => {
|
||||
if (jobList.length) {
|
||||
// lodash ``keyBy`` is significantly faster than doing a ``reduce``
|
||||
return {
|
||||
jobMap: { ...jobMap, ...keyBy(jobList, 'id') },
|
||||
decisionTaskMap: {
|
||||
...decisionTaskMap,
|
||||
...keyBy(
|
||||
jobList
|
||||
.filter(
|
||||
job =>
|
||||
job.job_type_name.includes('Decision Task') &&
|
||||
job.result === 'success',
|
||||
)
|
||||
.map(job => ({
|
||||
push_id: job.push_id,
|
||||
id: job.task_id,
|
||||
run: job.retry_id,
|
||||
})),
|
||||
'push_id',
|
||||
),
|
||||
},
|
||||
jobsLoaded: pushList.every(push => push.jobsLoaded),
|
||||
};
|
||||
}
|
||||
|
@ -357,6 +374,7 @@ export const updateRange = range => {
|
|||
export const initialState = {
|
||||
pushList: [],
|
||||
jobMap: {},
|
||||
decisionTaskMap: {},
|
||||
revisionTips: [],
|
||||
jobsLoaded: false,
|
||||
loadingPushes: true,
|
||||
|
@ -367,7 +385,7 @@ export const initialState = {
|
|||
|
||||
export const reducer = (state = initialState, action) => {
|
||||
const { jobList, pushResults, setFromchange } = action;
|
||||
const { pushList, jobMap } = state;
|
||||
const { pushList, jobMap, decisionTaskMap } = state;
|
||||
switch (action.type) {
|
||||
case LOADING:
|
||||
return { ...state, loadingPushes: true };
|
||||
|
@ -380,7 +398,10 @@ export const reducer = (state = initialState, action) => {
|
|||
case RECALCULATE_UNCLASSIFIED_COUNTS:
|
||||
return { ...state, ...doRecalculateUnclassifiedCounts(jobMap) };
|
||||
case UPDATE_JOB_MAP:
|
||||
return { ...state, ...doUpdateJobMap(jobList, jobMap, pushList) };
|
||||
return {
|
||||
...state,
|
||||
...doUpdateJobMap(jobList, jobMap, decisionTaskMap, pushList),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { slugid } from 'taskcluster-client-web';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
|
||||
import { createQueryParams, getApiUrl } from '../helpers/url';
|
||||
import { formatTaskclusterError } from '../helpers/errorMessage';
|
||||
|
@ -84,14 +83,22 @@ export default class JobModel {
|
|||
return JobModel.getList(options, config);
|
||||
}
|
||||
|
||||
static async retrigger(jobs, repoName, notify, times = 1) {
|
||||
static async retrigger(
|
||||
jobs,
|
||||
repoName,
|
||||
notify,
|
||||
times = 1,
|
||||
decisionTaskIdMap = null,
|
||||
) {
|
||||
const jobTerm = jobs.length > 1 ? 'jobs' : 'job';
|
||||
|
||||
try {
|
||||
notify(`Attempting to retrigger/add ${jobTerm} via actions.json`, 'info');
|
||||
|
||||
const pushIds = [...new Set(jobs.map(job => job.push_id))];
|
||||
const taskIdMap = await PushModel.getDecisionTaskMap(pushIds, notify);
|
||||
const taskIdMap =
|
||||
decisionTaskIdMap ||
|
||||
(await PushModel.getDecisionTaskMap(pushIds, notify));
|
||||
const uniquePerPushJobs = groupBy(jobs, job => job.push_id);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
@ -147,11 +154,10 @@ export default class JobModel {
|
|||
}
|
||||
}
|
||||
|
||||
static async cancelAll(pushId, repoName, notify) {
|
||||
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
|
||||
pushId,
|
||||
notify,
|
||||
);
|
||||
static async cancelAll(pushId, repoName, notify, decisionTask) {
|
||||
const { id: decisionTaskId } =
|
||||
decisionTask || (await PushModel.getDecisionTaskId(pushId, notify));
|
||||
|
||||
const results = await TaskclusterModel.load(decisionTaskId);
|
||||
|
||||
try {
|
||||
|
@ -172,27 +178,14 @@ export default class JobModel {
|
|||
notify('Request sent to cancel all jobs via action.json', 'success');
|
||||
}
|
||||
|
||||
static async cancel(jobs, repoName, notify) {
|
||||
static async cancel(jobs, repoName, notify, decisionTaskIdMap) {
|
||||
const jobTerm = jobs.length > 1 ? 'jobs' : 'job';
|
||||
const taskIdMap = await PushModel.getDecisionTaskMap(
|
||||
[...new Set(jobs.map(job => job.push_id))],
|
||||
notify,
|
||||
);
|
||||
// Only the selected job will have the ``taskcluster_metadata`` field
|
||||
// which has the task_id we need. So we must fetch all the task_ids
|
||||
// for the jobs in this list.
|
||||
const jobIds = jobs.map(job => job.id);
|
||||
const { data, failureStatus } = await getData(
|
||||
`${getApiUrl('/taskclustermetadata/')}?job_ids=${jobIds.join(',')}`,
|
||||
);
|
||||
|
||||
if (failureStatus) {
|
||||
notify('Unable to cancel: Error getting task ids for jobs.', 'danger', {
|
||||
sticky: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const tcMetadataMap = keyBy(data, 'job');
|
||||
const taskIdMap =
|
||||
decisionTaskIdMap ||
|
||||
(await PushModel.getDecisionTaskMap(
|
||||
[...new Set(jobs.map(job => job.push_id))],
|
||||
notify,
|
||||
));
|
||||
|
||||
try {
|
||||
notify(
|
||||
|
@ -203,7 +196,6 @@ export default class JobModel {
|
|||
/* eslint-disable no-await-in-loop */
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (const job of jobs) {
|
||||
job.taskcluster_metadata = tcMetadataMap[job.id];
|
||||
const decisionTaskId = taskIdMap[job.push_id].id;
|
||||
const results = await TaskclusterModel.load(decisionTaskId, job);
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ const convertDates = function convertDates(locationParams) {
|
|||
return locationParams;
|
||||
};
|
||||
|
||||
const decisionTaskIdCache = {};
|
||||
export const decisionTaskIdCache = {};
|
||||
|
||||
export default class PushModel {
|
||||
static async getList(options = {}) {
|
||||
|
@ -65,11 +65,9 @@ export default class PushModel {
|
|||
return fetch(getProjectUrl(`${pushEndpoint}${pk}/`, repoName));
|
||||
}
|
||||
|
||||
static async triggerMissingJobs(pushId, notify) {
|
||||
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
|
||||
pushId,
|
||||
notify,
|
||||
);
|
||||
static async triggerMissingJobs(pushId, notify, decisionTask) {
|
||||
const decisionTaskId =
|
||||
decisionTask || (await PushModel.getDecisionTaskId(pushId, notify)).id;
|
||||
|
||||
return TaskclusterModel.load(decisionTaskId).then(results => {
|
||||
const actionTaskId = slugid();
|
||||
|
@ -102,11 +100,9 @@ export default class PushModel {
|
|||
});
|
||||
}
|
||||
|
||||
static async triggerAllTalosJobs(times, pushId, notify) {
|
||||
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
|
||||
pushId,
|
||||
notify,
|
||||
);
|
||||
static async triggerAllTalosJobs(times, pushId, notify, decisionTask) {
|
||||
const decisionTaskId =
|
||||
decisionTask || (await PushModel.getDecisionTaskId(pushId, notify)).id;
|
||||
|
||||
return TaskclusterModel.load(decisionTaskId).then(results => {
|
||||
const actionTaskId = slugid();
|
||||
|
|
|
@ -8,7 +8,8 @@ export default class RunnableJobModel {
|
|||
}
|
||||
|
||||
static async getList(repoName, params) {
|
||||
const uri = getRunnableJobsURL(params.decision_task_id);
|
||||
const { push_id, decisionTask } = params;
|
||||
const uri = getRunnableJobsURL(decisionTask);
|
||||
const rawJobs = await fetch(uri).then(response => response.json());
|
||||
|
||||
return Object.entries(rawJobs).map(([key, value]) =>
|
||||
|
@ -24,8 +25,8 @@ export default class RunnableJobModel {
|
|||
signature: key,
|
||||
state: 'runnable',
|
||||
result: 'runnable',
|
||||
push_id: params.push_id,
|
||||
id: escapeId(params.push_id + key),
|
||||
push_id,
|
||||
id: escapeId(push_id + key),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -88,8 +88,8 @@ export default class TaskclusterModel {
|
|||
|
||||
let originalTaskId;
|
||||
let originalTaskPromise = Promise.resolve(null);
|
||||
if (job && job.taskcluster_metadata) {
|
||||
originalTaskId = job.taskcluster_metadata.task_id;
|
||||
if (job) {
|
||||
originalTaskId = job.task_id;
|
||||
originalTaskPromise = fetch(
|
||||
`https://queue.taskcluster.net/v1/task/${originalTaskId}`,
|
||||
).then(async response => response.json());
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class JobInfo extends React.PureComponent {
|
|||
const {
|
||||
signature,
|
||||
title,
|
||||
taskcluster_metadata,
|
||||
task_id,
|
||||
build_platform,
|
||||
job_type_name,
|
||||
build_architecture,
|
||||
|
@ -67,16 +67,16 @@ export default class JobInfo extends React.PureComponent {
|
|||
<span>{title}</span>
|
||||
)}
|
||||
</li>
|
||||
{taskcluster_metadata && (
|
||||
{task_id && (
|
||||
<li className="small">
|
||||
<strong>Task: </strong>
|
||||
<a
|
||||
id="taskInfo"
|
||||
href={getInspectTaskUrl(taskcluster_metadata.task_id)}
|
||||
href={getInspectTaskUrl(task_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{taskcluster_metadata.task_id}
|
||||
{task_id}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
|
|
Загрузка…
Ссылка в новой задаче