gitlab-radiator 3.3.11 → 3.4.0

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); 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 = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : 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 = defaultBranchStatus(project, 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);
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
+ }
149
33
  }
150
34
 
151
35
  function defaultBranchStatus(project, pipelines) {
152
- return (0, _lodash.default)(pipelines).filter({
153
- ref: project.default_branch
154
- }).map('status').head();
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,117 @@
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)
4
6
 
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); 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 = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : 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);
67
- }
68
-
69
- function fetchLatestAndMasterPipeline(_x3, _x4) {
70
- return _fetchLatestAndMasterPipeline.apply(this, arguments);
71
- }
72
-
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);
7
+ return Promise.all(pipelines.map(async ({id, ref, status}) => {
8
+ const {commit, stages} = await fetchJobs(projectId, id, gitlab)
9
+ const downstreamStages = await fetchDownstreamJobs(projectId, id, gitlab)
10
+ return {
11
+ id,
12
+ ref,
13
+ status,
14
+ commit,
15
+ stages: stages.concat(downstreamStages)
16
+ }
17
+ }))
127
18
  }
128
19
 
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
132
- // 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);
20
+ // eslint-disable-next-line max-statements
21
+ async function fetchLatestAndMasterPipeline(projectId, config) {
22
+ const pipelines = await fetchPipelines(projectId, config, {per_page: 100})
23
+ if (pipelines.length === 0) {
24
+ return []
25
+ }
26
+ const latestPipeline = _.take(pipelines, 1)
27
+ if (latestPipeline[0].ref === 'master') {
28
+ return latestPipeline
29
+ }
30
+ const latestMasterPipeline = _(pipelines).filter({ref: 'master'}).take(1).value()
31
+ if (latestMasterPipeline.length > 0) {
32
+ return latestPipeline.concat(latestMasterPipeline)
33
+ }
34
+ const masterPipelines = await fetchPipelines(projectId, config, {per_page: 50, ref: 'master'})
35
+ return latestPipeline.concat(_.take(masterPipelines, 1))
159
36
  }
160
37
 
161
- function fetchJobsForPipelines(_x8, _x9, _x10) {
162
- return _fetchJobsForPipelines.apply(this, arguments);
38
+ async function fetchPipelines(projectId, config, options) {
39
+ const {data: pipelines} = await gitlabRequest(`/projects/${projectId}/pipelines`, options, config)
40
+ return pipelines.filter(pipeline => pipeline.status !== 'skipped')
163
41
  }
164
42
 
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);
43
+ async function fetchDownstreamJobs(projectId, pipelineId, config) {
44
+ const {data: gitlabBridgeJobs} = await gitlabRequest(`/projects/${projectId}/pipelines/${pipelineId}/bridges`, {per_page: 100}, config)
45
+ const childPipelines = gitlabBridgeJobs.filter(bridge => bridge.downstream_pipeline.status !== 'skipped')
46
+
47
+ const downstreamStages = []
48
+ for(const childPipeline of childPipelines) {
49
+ const {stages} = await fetchJobs(projectId, childPipeline.downstream_pipeline.id, config)
50
+ downstreamStages.push(stages.map(stage => ({
51
+ ...stage,
52
+ name: `${childPipeline.stage}:${stage.name}`
53
+ })))
54
+ }
55
+ return downstreamStages.flat()
219
56
  }
220
57
 
221
- function matchJobs(pipelineId, gitlabJobsForMultiplePipelines) {
222
- var gitlabJobs = gitlabJobsForMultiplePipelines.filter(job => job.pipeline.id === pipelineId);
223
-
58
+ async function fetchJobs(projectId, pipelineId, config) {
59
+ const {data: gitlabJobs} = await gitlabRequest(`/projects/${projectId}/pipelines/${pipelineId}/jobs?include_retried=true`, {per_page: 100}, config)
224
60
  if (gitlabJobs.length === 0) {
225
- return {};
61
+ return {}
226
62
  }
227
63
 
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];
64
+ const commit = findCommit(gitlabJobs)
65
+ const stages = _(gitlabJobs)
66
+ .map(job => ({
67
+ id: job.id,
68
+ status: job.status,
69
+ stage: job.stage,
70
+ name: job.name,
71
+ startedAt: job.started_at,
72
+ finishedAt: job.finished_at,
73
+ url: job.web_url
74
+ }))
75
+ .orderBy('id')
76
+ .groupBy('stage')
77
+ .mapValues(mergeRetriedJobs)
78
+ .mapValues(cleanup)
79
+ .toPairs()
80
+ .map(([name, jobs]) => ({name, jobs: _.sortBy(jobs, 'name')}))
81
+ .value()
241
82
 
242
- return {
243
- name,
244
- jobs: _lodash.default.sortBy(jobs, 'name')
245
- };
246
- }).value();
247
83
  return {
248
84
  commit,
249
85
  stages
250
- };
86
+ }
251
87
  }
252
88
 
253
89
  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
-
90
+ const [job] = jobs.filter(j => j.commit)
258
91
  if (!job) {
259
- return null;
92
+ return null
260
93
  }
261
-
262
94
  return {
263
95
  title: job.commit.title,
264
96
  author: job.commit.author_name
265
- };
97
+ }
266
98
  }
267
99
 
268
100
  function mergeRetriedJobs(jobs) {
269
101
  return jobs.reduce((mergedJobs, job) => {
270
- var index = mergedJobs.findIndex(mergedJob => mergedJob.name === job.name);
271
-
102
+ const index = mergedJobs.findIndex(mergedJob => mergedJob.name === job.name)
272
103
  if (index >= 0) {
273
- mergedJobs[index] = job;
104
+ mergedJobs[index] = job
274
105
  } else {
275
- mergedJobs.push(job);
106
+ mergedJobs.push(job)
276
107
  }
277
-
278
- return mergedJobs;
279
- }, []);
108
+ return mergedJobs
109
+ }, [])
280
110
  }
281
111
 
282
112
  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
- }
113
+ return _(jobs)
114
+ .map(job => _.omitBy(job, _.isNull))
115
+ .map(job => _.omit(job, 'stage'))
116
+ .value()
117
+ }