• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

screwdriver-cd / screwdriver / #3202

25 Jul 2025 04:52PM UTC coverage: 67.669% (-27.3%) from 94.935%
#3202

push

screwdriver

web-flow
feat(3363): Update the existing endpoint to get admin for a pipeline from the specified SCM context (#3370)

1284 of 2114 branches covered (60.74%)

Branch coverage included in aggregate %.

1 of 11 new or added lines in 1 file covered. (9.09%)

1235 existing lines in 49 files now uncovered.

3417 of 4833 relevant lines covered (70.7%)

50.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

7.03
/plugins/pipelines/update.js
1
'use strict';
2

3
const boom = require('@hapi/boom');
1✔
4
const joi = require('joi');
1✔
5
const schema = require('screwdriver-data-schema');
1✔
6
const logger = require('screwdriver-logger');
1✔
7
const idSchema = schema.models.pipeline.base.extract('id');
1✔
8
const { formatCheckoutUrl, sanitizeRootDir } = require('./helper');
1✔
9
const { getUserPermissions } = require('../helper');
1✔
10
const ANNOTATION_USE_DEPLOY_KEY = 'screwdriver.cd/useDeployKey';
1✔
11

12
/**
13
 * Get user permissions on old pipeline
14
 * @method getPermissionsForOldPipeline
15
 * @param  {Array}                     scmContexts  An array of scmContext
16
 * @param  {Object}                    pipeline     Pipeline to check against
17
 * @param  {Object}                    user         User to check for
18
 * @return {Promise}
19
 */
20
function getPermissionsForOldPipeline({ scmContexts, pipeline, user }) {
UNCOV
21
    const isPipelineSCMContextObsolete = !scmContexts.includes(pipeline.scmContext);
×
UNCOV
22
    const isUserFromAnotherSCMContext = user.scmContext !== pipeline.scmContext;
×
23

24
    // for mysql backward compatibility
UNCOV
25
    if (!pipeline.adminUserIds) {
×
26
        pipeline.adminUserIds = [];
×
27
    }
28
    // this pipeline's scmContext has been removed, allow current admin to change it
29
    // also allow pipeline admins from other scmContexts to change it
UNCOV
30
    if (isPipelineSCMContextObsolete || isUserFromAnotherSCMContext) {
×
UNCOV
31
        const isUserIdInAdminList = pipeline.adminUserIds.includes(user.id);
×
UNCOV
32
        const isSCMUsernameInAdminsObject = !!pipeline.admins[user.username];
×
33

UNCOV
34
        const isAdmin = isUserIdInAdminList || (isPipelineSCMContextObsolete && isSCMUsernameInAdminsObject);
×
35

UNCOV
36
        return Promise.resolve({ admin: isAdmin });
×
37
    }
38

UNCOV
39
    return user.getPermissions(pipeline.scmUri);
×
40
}
41

42
module.exports = () => ({
434✔
43
    method: 'PUT',
44
    path: '/pipelines/{id}',
45
    options: {
46
        description: 'Update a pipeline',
47
        notes: 'Update a specific pipeline',
48
        tags: ['api', 'pipelines'],
49
        auth: {
50
            strategies: ['token'],
51
            scope: ['user', '!guest', 'pipeline']
52
        },
53

54
        handler: async (request, h) => {
UNCOV
55
            const { checkoutUrl, rootDir, settings, badges } = request.payload;
×
UNCOV
56
            const { id } = request.params;
×
UNCOV
57
            const { pipelineFactory, userFactory, secretFactory } = request.server.app;
×
UNCOV
58
            const { scmContext, username } = request.auth.credentials;
×
UNCOV
59
            const scmContexts = pipelineFactory.scm.getScmContexts();
×
UNCOV
60
            const { isValidToken } = request.server.plugins.pipelines;
×
UNCOV
61
            const deployKeySecret = 'SD_SCM_DEPLOY_KEY';
×
62

UNCOV
63
            if (!isValidToken(id, request.auth.credentials)) {
×
UNCOV
64
                return boom.unauthorized('Token does not have permission to this pipeline');
×
65
            }
66

67
            // get the pipeline given its ID and the user
UNCOV
68
            const oldPipeline = await pipelineFactory.get({ id });
×
UNCOV
69
            const user = await userFactory.get({ username, scmContext });
×
70

71
            // Handle pipeline permissions
72
            // if the pipeline ID is invalid, reject
UNCOV
73
            if (!oldPipeline) {
×
UNCOV
74
                throw boom.notFound(`Pipeline ${id} does not exist`);
×
75
            }
UNCOV
76
            if (oldPipeline.state === 'DELETING') {
×
UNCOV
77
                throw boom.conflict('This pipeline is being deleted.');
×
78
            }
79

80
            // for mysql backward compatibility
UNCOV
81
            if (!oldPipeline.adminUserIds) {
×
82
                oldPipeline.adminUserIds = [];
×
83
            }
84

UNCOV
85
            if (oldPipeline.configPipelineId) {
×
UNCOV
86
                throw boom.forbidden(
×
87
                    `Child pipeline can only be modified by config pipeline ${oldPipeline.configPipelineId}`
88
                );
89
            }
90

91
            // get the user permissions for the repo
92
            let oldPermissions;
93

UNCOV
94
            try {
×
UNCOV
95
                oldPermissions = await getPermissionsForOldPipeline({
×
96
                    scmContexts,
97
                    pipeline: oldPipeline,
98
                    user
99
                });
100
            } catch (err) {
UNCOV
101
                throw boom.forbidden(`User ${user.getFullDisplayName()} does not have admin permission for this repo`);
×
102
            }
103

104
            let token;
105
            let formattedCheckoutUrl;
UNCOV
106
            const oldPipelineConfig = { ...oldPipeline };
×
107

UNCOV
108
            if (checkoutUrl || rootDir) {
×
UNCOV
109
                formattedCheckoutUrl = formatCheckoutUrl(request.payload.checkoutUrl);
×
UNCOV
110
                const sanitizedRootDir = sanitizeRootDir(request.payload.rootDir);
×
111

112
                // get the user token
UNCOV
113
                token = await user.unsealToken();
×
114
                // get the scm URI
UNCOV
115
                const scmUri = await pipelineFactory.scm.parseUrl({
×
116
                    scmContext,
117
                    checkoutUrl: formattedCheckoutUrl,
118
                    rootDir: sanitizedRootDir,
119
                    token
120
                });
121

122
                // get the user permissions for the repo
UNCOV
123
                await getUserPermissions({ user, scmUri });
×
124

125
                // check if there is already a pipeline with the new checkoutUrl
UNCOV
126
                const newPipeline = await pipelineFactory.get({ scmUri });
×
127

128
                // reject if pipeline already exists with new checkoutUrl
UNCOV
129
                if (newPipeline) {
×
UNCOV
130
                    throw boom.conflict(`Pipeline already exists with the ID: ${newPipeline.id}`);
×
131
                }
132

UNCOV
133
                const scmRepo = await pipelineFactory.scm.decorateUrl({
×
134
                    scmUri,
135
                    scmContext,
136
                    token
137
                });
138

139
                // update keys
UNCOV
140
                oldPipeline.scmContext = scmContext;
×
UNCOV
141
                oldPipeline.scmUri = scmUri;
×
UNCOV
142
                oldPipeline.scmRepo = scmRepo;
×
UNCOV
143
                oldPipeline.name = scmRepo.name;
×
144
            }
145

UNCOV
146
            if (!oldPermissions.admin) {
×
UNCOV
147
                throw boom.forbidden(`User ${username} is not an admin of these repos`);
×
148
            }
149

UNCOV
150
            oldPipeline.admins = {
×
151
                [username]: true
152
            };
153

UNCOV
154
            if (!oldPipeline.adminUserIds.includes(user.id)) {
×
UNCOV
155
                oldPipeline.adminUserIds.push(user.id);
×
156
            }
157

UNCOV
158
            if (settings) {
×
UNCOV
159
                oldPipeline.settings = { ...oldPipeline.settings, ...settings };
×
160
            }
161

UNCOV
162
            if (checkoutUrl || rootDir) {
×
UNCOV
163
                logger.info(
×
164
                    `[Audit] user ${user.username}:${scmContext} updates the scmUri for pipelineID:${id} to ${oldPipeline.scmUri} from ${oldPipelineConfig.scmUri}.`
165
                );
166
            }
167

UNCOV
168
            if (badges) {
×
UNCOV
169
                if (!oldPipeline.badges) {
×
UNCOV
170
                    oldPipeline.badges = badges;
×
171
                } else {
172
                    const newBadges = {};
×
173

174
                    Object.keys(oldPipeline.badges).forEach(badgeKey => {
×
175
                        newBadges[badgeKey] = {
×
176
                            ...oldPipeline.badges[badgeKey],
177
                            ...badges[badgeKey]
178
                        };
179
                    });
180

181
                    oldPipeline.badges = newBadges;
×
182
                }
183
            }
184

185
            // update pipeline
UNCOV
186
            const updatedPipeline = await oldPipeline.update();
×
187

UNCOV
188
            await updatedPipeline.addWebhooks(`${request.server.info.uri}/v4/webhooks`);
×
189

UNCOV
190
            const result = await updatedPipeline.sync();
×
191

192
            // check if pipeline has deploy key annotation then create secrets
193
            // sync needs to happen before checking annotations
194
            const deployKeyAnnotation =
UNCOV
195
                updatedPipeline.annotations && updatedPipeline.annotations[ANNOTATION_USE_DEPLOY_KEY];
×
196

UNCOV
197
            if (deployKeyAnnotation) {
×
UNCOV
198
                const deploySecret = await secretFactory.get({
×
199
                    pipelineId: updatedPipeline.id,
200
                    name: deployKeySecret
201
                });
202
                // create only secret doesn't exist already
203

UNCOV
204
                if (!deploySecret) {
×
UNCOV
205
                    const privateDeployKey = await pipelineFactory.scm.addDeployKey({
×
206
                        scmContext: updatedPipeline.scmContext,
207
                        checkoutUrl: formattedCheckoutUrl,
208
                        token
209
                    });
UNCOV
210
                    const privateDeployKeyB64 = Buffer.from(privateDeployKey).toString('base64');
×
211

UNCOV
212
                    await secretFactory.create({
×
213
                        pipelineId: updatedPipeline.id,
214
                        name: deployKeySecret,
215
                        value: privateDeployKeyB64,
216
                        allowInPR: true
217
                    });
218
                }
219
            }
220

UNCOV
221
            return h.response(result.toJson()).code(200);
×
222
        },
223
        validate: {
224
            params: joi.object({
225
                id: idSchema
226
            }),
227
            payload: schema.models.pipeline.update
228
        }
229
    }
230
});
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc