gitlab-radiator 3.3.8 → 3.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,169 +1,53 @@
1
- "use strict";
2
-
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
-
5
- Object.defineProperty(exports, "__esModule", {
6
- value: true
7
- });
8
- exports.update = update;
9
-
10
- var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11
-
12
- var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
13
-
14
- var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
15
-
16
- var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
17
-
18
- var _lodash = _interopRequireDefault(require("lodash"));
19
-
20
- var _pipelines = require("./pipelines");
21
-
22
- var _projects = require("./projects");
23
-
24
- function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
25
-
26
- function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
27
-
28
- function update(_x) {
29
- return _update.apply(this, arguments);
30
- }
31
-
32
- function _update() {
33
- _update = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(config) {
34
- var projectsWithPipelines;
35
- return _regenerator.default.wrap(function _callee$(_context) {
36
- while (1) {
37
- switch (_context.prev = _context.next) {
38
- case 0:
39
- _context.next = 2;
40
- return loadProjectsWithPipelines(config);
41
-
42
- case 2:
43
- projectsWithPipelines = _context.sent;
44
- return _context.abrupt("return", projectsWithPipelines.filter(project => project.pipelines.length > 0));
45
-
46
- case 4:
47
- case "end":
48
- return _context.stop();
49
- }
50
- }
51
- }, _callee);
52
- }));
53
- return _update.apply(this, arguments);
54
- }
55
-
56
- function loadProjectsWithPipelines(_x2) {
57
- return _loadProjectsWithPipelines.apply(this, arguments);
58
- }
59
-
60
- function _loadProjectsWithPipelines() {
61
- _loadProjectsWithPipelines = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(config) {
62
- var allProjectsWithPipelines;
63
- return _regenerator.default.wrap(function _callee3$(_context3) {
64
- while (1) {
65
- switch (_context3.prev = _context3.next) {
66
- case 0:
67
- allProjectsWithPipelines = [];
68
- _context3.next = 3;
69
- return Promise.all(config.gitlabs.map( /*#__PURE__*/function () {
70
- var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(gitlab) {
71
- var projects, projectsWithPipelines;
72
- return _regenerator.default.wrap(function _callee2$(_context2) {
73
- while (1) {
74
- switch (_context2.prev = _context2.next) {
75
- case 0:
76
- _context2.next = 2;
77
- return (0, _projects.fetchProjects)(gitlab);
78
-
79
- case 2:
80
- projects = _context2.sent;
81
- projects.forEach(project => {
82
- project.maxNonFailedJobsVisible = gitlab.maxNonFailedJobsVisible;
83
- });
84
- _context2.next = 6;
85
- return Promise.all(projects.map(project => projectWithPipelines(project, gitlab)));
86
-
87
- case 6:
88
- projectsWithPipelines = _context2.sent;
89
- allProjectsWithPipelines.push.apply(allProjectsWithPipelines, (0, _toConsumableArray2.default)(projectsWithPipelines));
90
-
91
- case 8:
92
- case "end":
93
- return _context2.stop();
94
- }
95
- }
96
- }, _callee2);
97
- }));
98
-
99
- return function (_x5) {
100
- return _ref.apply(this, arguments);
101
- };
102
- }()));
103
-
104
- case 3:
105
- return _context3.abrupt("return", allProjectsWithPipelines);
106
-
107
- case 4:
108
- case "end":
109
- return _context3.stop();
110
- }
111
- }
112
- }, _callee3);
113
- }));
114
- return _loadProjectsWithPipelines.apply(this, arguments);
115
- }
116
-
117
- function projectWithPipelines(_x3, _x4) {
118
- return _projectWithPipelines.apply(this, arguments);
119
- }
120
-
121
- function _projectWithPipelines() {
122
- _projectWithPipelines = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4(project, config) {
123
- var pipelines, status;
124
- return _regenerator.default.wrap(function _callee4$(_context4) {
125
- while (1) {
126
- switch (_context4.prev = _context4.next) {
127
- case 0:
128
- _context4.t0 = filterOutEmpty;
129
- _context4.next = 3;
130
- return (0, _pipelines.fetchLatestPipelines)(project.id, config);
131
-
132
- case 3:
133
- _context4.t1 = _context4.sent;
134
- pipelines = (0, _context4.t0)(_context4.t1).filter(excludePipelineStatusFilter(config));
135
- status = masterBranchStatus(pipelines);
136
- return _context4.abrupt("return", _objectSpread(_objectSpread({}, project), {}, {
137
- pipelines,
138
- status
139
- }));
140
-
141
- case 7:
142
- case "end":
143
- return _context4.stop();
144
- }
145
- }
146
- }, _callee4);
147
- }));
148
- return _projectWithPipelines.apply(this, arguments);
149
- }
150
-
151
- function masterBranchStatus(pipelines) {
152
- return (0, _lodash.default)(pipelines).filter({
153
- ref: 'master'
154
- }).map('status').head();
1
+ import _ from 'lodash'
2
+ import {fetchLatestPipelines} from './pipelines'
3
+ import {fetchProjects} from './projects'
4
+
5
+ export async function update(config) {
6
+ const projectsWithPipelines = await loadProjectsWithPipelines(config)
7
+ return projectsWithPipelines
8
+ .filter(project => project.pipelines.length > 0)
9
+ }
10
+
11
+ async function loadProjectsWithPipelines(config) {
12
+ const allProjectsWithPipelines = []
13
+ await Promise.all(config.gitlabs.map(async (gitlab) => {
14
+ const projects = await fetchProjects(gitlab)
15
+ projects.forEach((project) => {
16
+ project.maxNonFailedJobsVisible = gitlab.maxNonFailedJobsVisible
17
+ })
18
+ const projectsWithPipelines = await Promise.all(projects.map(project => projectWithPipelines(project, gitlab)))
19
+ allProjectsWithPipelines.push(...projectsWithPipelines)
20
+ }))
21
+ return allProjectsWithPipelines
22
+ }
23
+
24
+ async function projectWithPipelines(project, config) {
25
+ const pipelines = filterOutEmpty(await fetchLatestPipelines(project.id, config))
26
+ .filter(excludePipelineStatusFilter(config))
27
+ const status = defaultBranchStatus(project, pipelines)
28
+ return {
29
+ ...project,
30
+ pipelines,
31
+ status
32
+ }
33
+ }
34
+
35
+ function defaultBranchStatus(project, pipelines) {
36
+ return _(pipelines)
37
+ .filter({ref: project.default_branch})
38
+ .map('status')
39
+ .head()
155
40
  }
156
41
 
157
42
  function filterOutEmpty(pipelines) {
158
- return pipelines.filter(pipeline => pipeline.stages);
43
+ return pipelines.filter(pipeline => pipeline.stages)
159
44
  }
160
45
 
161
46
  function excludePipelineStatusFilter(config) {
162
47
  return pipeline => {
163
48
  if (config.projects && config.projects.excludePipelineStatus) {
164
- return !config.projects.excludePipelineStatus.includes(pipeline.status);
49
+ return !config.projects.excludePipelineStatus.includes(pipeline.status)
165
50
  }
166
-
167
- return true;
168
- };
169
- }
51
+ return true
52
+ }
53
+ }
@@ -1,284 +1,116 @@
1
- "use strict";
1
+ import _ from 'lodash'
2
+ import {gitlabRequest} from './client'
2
3
 
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ export async function fetchLatestPipelines(projectId, gitlab) {
5
+ const pipelines = await fetchLatestAndMasterPipeline(projectId, gitlab)
6
+ const jobsForPipelines = await fetchJobsForPipelines(projectId, pipelines, gitlab)
4
7
 
5
- Object.defineProperty(exports, "__esModule", {
6
- value: true
7
- });
8
- exports.fetchLatestPipelines = fetchLatestPipelines;
9
-
10
- var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11
-
12
- var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
13
-
14
- var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
15
-
16
- var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
17
-
18
- var _lodash = _interopRequireDefault(require("lodash"));
19
-
20
- var _client = require("./client");
21
-
22
- function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
23
-
24
- function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
25
-
26
- function fetchLatestPipelines(_x, _x2) {
27
- return _fetchLatestPipelines.apply(this, arguments);
28
- }
29
-
30
- function _fetchLatestPipelines() {
31
- _fetchLatestPipelines = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(projectId, gitlab) {
32
- var pipelines, jobsForPipelines;
33
- return _regenerator.default.wrap(function _callee$(_context) {
34
- while (1) {
35
- switch (_context.prev = _context.next) {
36
- case 0:
37
- _context.next = 2;
38
- return fetchLatestAndMasterPipeline(projectId, gitlab);
39
-
40
- case 2:
41
- pipelines = _context.sent;
42
- _context.next = 5;
43
- return fetchJobsForPipelines(projectId, pipelines, gitlab);
44
-
45
- case 5:
46
- jobsForPipelines = _context.sent;
47
- return _context.abrupt("return", pipelines.map(_ref3 => {
48
- var id = _ref3.id,
49
- ref = _ref3.ref,
50
- status = _ref3.status;
51
- var jobs = matchJobs(id, jobsForPipelines);
52
- return _objectSpread({
53
- id,
54
- ref,
55
- status
56
- }, jobs);
57
- }));
58
-
59
- case 7:
60
- case "end":
61
- return _context.stop();
62
- }
63
- }
64
- }, _callee);
65
- }));
66
- return _fetchLatestPipelines.apply(this, arguments);
8
+ return pipelines.map(({id, ref, status}) => {
9
+ const jobs = matchJobs(id, jobsForPipelines)
10
+ return {
11
+ id,
12
+ ref,
13
+ status,
14
+ ...jobs
15
+ }
16
+ })
67
17
  }
68
18
 
69
- function fetchLatestAndMasterPipeline(_x3, _x4) {
70
- return _fetchLatestAndMasterPipeline.apply(this, arguments);
19
+ async function fetchLatestAndMasterPipeline(projectId, config) {
20
+ const [latestPipeline] = await fetchNonSkippedPipelines(projectId, config, {per_page: 100})
21
+ if (!latestPipeline) {
22
+ return []
23
+ }
24
+ if (latestPipeline.ref === 'master') {
25
+ return [latestPipeline]
26
+ }
27
+ const [latestMasterPipeline] = await fetchNonSkippedPipelines(projectId, config, {per_page: 50, ref: 'master'})
28
+ return [latestPipeline].concat(latestMasterPipeline || [])
71
29
  }
72
30
 
73
- function _fetchLatestAndMasterPipeline() {
74
- _fetchLatestAndMasterPipeline = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(projectId, config) {
75
- var _yield$fetchNonSkippe, _yield$fetchNonSkippe2, latestPipeline, _yield$fetchNonSkippe3, _yield$fetchNonSkippe4, latestMasterPipeline;
76
-
77
- return _regenerator.default.wrap(function _callee2$(_context2) {
78
- while (1) {
79
- switch (_context2.prev = _context2.next) {
80
- case 0:
81
- _context2.next = 2;
82
- return fetchNonSkippedPipelines(projectId, config, {
83
- per_page: 100
84
- });
85
-
86
- case 2:
87
- _yield$fetchNonSkippe = _context2.sent;
88
- _yield$fetchNonSkippe2 = (0, _slicedToArray2.default)(_yield$fetchNonSkippe, 1);
89
- latestPipeline = _yield$fetchNonSkippe2[0];
90
-
91
- if (latestPipeline) {
92
- _context2.next = 7;
93
- break;
94
- }
95
-
96
- return _context2.abrupt("return", []);
97
-
98
- case 7:
99
- if (!(latestPipeline.ref === 'master')) {
100
- _context2.next = 9;
101
- break;
102
- }
103
-
104
- return _context2.abrupt("return", [latestPipeline]);
105
-
106
- case 9:
107
- _context2.next = 11;
108
- return fetchNonSkippedPipelines(projectId, config, {
109
- per_page: 50,
110
- ref: 'master'
111
- });
112
-
113
- case 11:
114
- _yield$fetchNonSkippe3 = _context2.sent;
115
- _yield$fetchNonSkippe4 = (0, _slicedToArray2.default)(_yield$fetchNonSkippe3, 1);
116
- latestMasterPipeline = _yield$fetchNonSkippe4[0];
117
- return _context2.abrupt("return", [latestPipeline].concat(latestMasterPipeline || []));
118
-
119
- case 15:
120
- case "end":
121
- return _context2.stop();
122
- }
123
- }
124
- }, _callee2);
125
- }));
126
- return _fetchLatestAndMasterPipeline.apply(this, arguments);
31
+ async function fetchNonSkippedPipelines(projectId, config, options) {
32
+ const {data: pipelines} = await gitlabRequest(`/projects/${projectId}/pipelines`, options, config)
33
+ return pipelines.filter(pipeline => pipeline.status !== 'skipped')
127
34
  }
128
35
 
129
- function fetchNonSkippedPipelines(_x5, _x6, _x7) {
130
- return _fetchNonSkippedPipelines.apply(this, arguments);
131
- } // GitLab API endpoint `/projects/${projectId}/pipelines/${pipelineId}/jobs` is broken and not returning all jobs
36
+ // GitLab API endpoint `/projects/${projectId}/pipelines/${pipelineId}/jobs` is broken and not returning all jobs
132
37
  // Need to fetch all jobs for the project (from newer to older) and match later
133
-
134
-
135
- function _fetchNonSkippedPipelines() {
136
- _fetchNonSkippedPipelines = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(projectId, config, options) {
137
- var _yield$gitlabRequest, pipelines;
138
-
139
- return _regenerator.default.wrap(function _callee3$(_context3) {
140
- while (1) {
141
- switch (_context3.prev = _context3.next) {
142
- case 0:
143
- _context3.next = 2;
144
- return (0, _client.gitlabRequest)("/projects/".concat(projectId, "/pipelines"), options, config);
145
-
146
- case 2:
147
- _yield$gitlabRequest = _context3.sent;
148
- pipelines = _yield$gitlabRequest.data;
149
- return _context3.abrupt("return", pipelines.filter(pipeline => pipeline.status !== 'skipped'));
150
-
151
- case 5:
152
- case "end":
153
- return _context3.stop();
154
- }
155
- }
156
- }, _callee3);
157
- }));
158
- return _fetchNonSkippedPipelines.apply(this, arguments);
159
- }
160
-
161
- function fetchJobsForPipelines(_x8, _x9, _x10) {
162
- return _fetchJobsForPipelines.apply(this, arguments);
163
- }
164
-
165
- function _fetchJobsForPipelines() {
166
- _fetchJobsForPipelines = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4(projectId, pipelines, config) {
167
- var includedPipelineIds, _pipelines$map$sort, _pipelines$map$sort2, oldestCreatedAt, jobs, SAFETY_MAX_PAGE, page, _yield$gitlabRequest2, jobsBatch;
168
-
169
- return _regenerator.default.wrap(function _callee4$(_context4) {
170
- while (1) {
171
- switch (_context4.prev = _context4.next) {
172
- case 0:
173
- includedPipelineIds = pipelines.map(pipeline => pipeline.id);
174
- _pipelines$map$sort = pipelines.map(pipeline => pipeline.created_at).sort(), _pipelines$map$sort2 = (0, _slicedToArray2.default)(_pipelines$map$sort, 1), oldestCreatedAt = _pipelines$map$sort2[0];
175
- jobs = [];
176
- SAFETY_MAX_PAGE = 10;
177
- page = 1;
178
-
179
- case 5:
180
- if (!(page <= SAFETY_MAX_PAGE)) {
181
- _context4.next = 16;
182
- break;
183
- }
184
-
185
- _context4.next = 8;
186
- return (0, _client.gitlabRequest)("/projects/".concat(projectId, "/jobs"), {
187
- page,
188
- per_page: 100
189
- }, config);
190
-
191
- case 8:
192
- _yield$gitlabRequest2 = _context4.sent;
193
- jobsBatch = _yield$gitlabRequest2.data;
194
- jobs.push(jobsBatch.filter(job => includedPipelineIds.includes(job.pipeline.id)).filter(job => job.created_at >= oldestCreatedAt));
195
-
196
- if (!(jobsBatch.length === 0 || jobsBatch[jobsBatch.length - 1].created_at < oldestCreatedAt)) {
197
- _context4.next = 13;
198
- break;
199
- }
200
-
201
- return _context4.abrupt("break", 16);
202
-
203
- case 13:
204
- page += 1;
205
- _context4.next = 5;
206
- break;
207
-
208
- case 16:
209
- return _context4.abrupt("return", jobs.flat());
210
-
211
- case 17:
212
- case "end":
213
- return _context4.stop();
214
- }
215
- }
216
- }, _callee4);
217
- }));
218
- return _fetchJobsForPipelines.apply(this, arguments);
38
+ async function fetchJobsForPipelines(projectId, pipelines, config) {
39
+ const includedPipelineIds = pipelines.map(pipeline => pipeline.id)
40
+ const [oldestCreatedAt] = pipelines.map(pipeline => pipeline.created_at).sort()
41
+
42
+ const jobs = []
43
+ const SAFETY_MAX_PAGE = 10
44
+ for (let page = 1; page <= SAFETY_MAX_PAGE; page += 1) {
45
+ // eslint-disable-next-line no-await-in-loop
46
+ const {data: jobsBatch} = await gitlabRequest(`/projects/${projectId}/jobs`, {page, per_page: 100}, config)
47
+ jobs.push(jobsBatch
48
+ .filter(job => includedPipelineIds.includes(job.pipeline.id))
49
+ .filter(job => job.created_at >= oldestCreatedAt))
50
+ if (jobsBatch.length === 0 || jobsBatch[jobsBatch.length - 1].created_at < oldestCreatedAt) {
51
+ break
52
+ }
53
+ }
54
+ return jobs.flat()
219
55
  }
220
56
 
221
57
  function matchJobs(pipelineId, gitlabJobsForMultiplePipelines) {
222
- var gitlabJobs = gitlabJobsForMultiplePipelines.filter(job => job.pipeline.id === pipelineId);
223
-
58
+ const gitlabJobs = gitlabJobsForMultiplePipelines.filter(job => job.pipeline.id === pipelineId)
224
59
  if (gitlabJobs.length === 0) {
225
- return {};
60
+ return {}
226
61
  }
227
62
 
228
- var commit = findCommit(gitlabJobs);
229
- var stages = (0, _lodash.default)(gitlabJobs).map(job => ({
230
- id: job.id,
231
- status: job.status,
232
- stage: job.stage,
233
- name: job.name,
234
- startedAt: job.started_at,
235
- finishedAt: job.finished_at,
236
- url: job.web_url
237
- })).orderBy('id').groupBy('stage').mapValues(mergeRetriedJobs).mapValues(cleanup).toPairs().map(_ref => {
238
- var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
239
- name = _ref2[0],
240
- jobs = _ref2[1];
63
+ const commit = findCommit(gitlabJobs)
64
+ const stages = _(gitlabJobs)
65
+ .map(job => ({
66
+ id: job.id,
67
+ status: job.status,
68
+ stage: job.stage,
69
+ name: job.name,
70
+ startedAt: job.started_at,
71
+ finishedAt: job.finished_at,
72
+ url: job.web_url
73
+ }))
74
+ .orderBy('id')
75
+ .groupBy('stage')
76
+ .mapValues(mergeRetriedJobs)
77
+ .mapValues(cleanup)
78
+ .toPairs()
79
+ .map(([name, jobs]) => ({name, jobs: _.sortBy(jobs, 'name')}))
80
+ .value()
241
81
 
242
- return {
243
- name,
244
- jobs: _lodash.default.sortBy(jobs, 'name')
245
- };
246
- }).value();
247
82
  return {
248
83
  commit,
249
84
  stages
250
- };
85
+ }
251
86
  }
252
87
 
253
88
  function findCommit(jobs) {
254
- var _jobs$filter = jobs.filter(j => j.commit),
255
- _jobs$filter2 = (0, _slicedToArray2.default)(_jobs$filter, 1),
256
- job = _jobs$filter2[0];
257
-
89
+ const [job] = jobs.filter(j => j.commit)
258
90
  if (!job) {
259
- return null;
91
+ return null
260
92
  }
261
-
262
93
  return {
263
94
  title: job.commit.title,
264
95
  author: job.commit.author_name
265
- };
96
+ }
266
97
  }
267
98
 
268
99
  function mergeRetriedJobs(jobs) {
269
100
  return jobs.reduce((mergedJobs, job) => {
270
- var index = mergedJobs.findIndex(mergedJob => mergedJob.name === job.name);
271
-
101
+ const index = mergedJobs.findIndex(mergedJob => mergedJob.name === job.name)
272
102
  if (index >= 0) {
273
- mergedJobs[index] = job;
103
+ mergedJobs[index] = job
274
104
  } else {
275
- mergedJobs.push(job);
105
+ mergedJobs.push(job)
276
106
  }
277
-
278
- return mergedJobs;
279
- }, []);
107
+ return mergedJobs
108
+ }, [])
280
109
  }
281
110
 
282
111
  function cleanup(jobs) {
283
- return (0, _lodash.default)(jobs).map(job => _lodash.default.omitBy(job, _lodash.default.isNull)).map(job => _lodash.default.omit(job, 'stage')).value();
284
- }
112
+ return _(jobs)
113
+ .map(job => _.omitBy(job, _.isNull))
114
+ .map(job => _.omit(job, 'stage'))
115
+ .value()
116
+ }