UNPKG

11.8 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright © 2019 Atomist, Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17Object.defineProperty(exports, "__esModule", { value: true });
18exports.DefaultExtractAuthor = exports.NoOpExtractAuthor = exports.AutofixProgressReporter = exports.AutofixProgressTests = exports.generateCommitMessageForAutofix = exports.filterImmediateAutofixes = exports.executeAutofixes = void 0;
19const HandlerResult_1 = require("@atomist/automation-client/lib/HandlerResult");
20const editModes_1 = require("@atomist/automation-client/lib/operations/edit/editModes");
21const projectEditorOps_1 = require("@atomist/automation-client/lib/operations/edit/projectEditorOps");
22const logger_1 = require("@atomist/automation-client/lib/util/logger");
23const slack_messages_1 = require("@atomist/slack-messages");
24const _ = require("lodash");
25const types_1 = require("../../typings/types");
26const confirmEditedness_1 = require("../command/transform/confirmEditedness");
27const minimalClone_1 = require("../goal/minimalClone");
28const progress_1 = require("../goal/progress/progress");
29const handlerRegistrations_1 = require("../machine/handlerRegistrations");
30const child_process_1 = require("../misc/child_process");
31const createPushImpactListenerInvocation_1 = require("./createPushImpactListenerInvocation");
32const relevantCodeActions_1 = require("./relevantCodeActions");
33/**
34 * Execute autofixes against this push
35 * Throw an error on failure
36 * @param {AutofixRegistration[]} registrations
37 * @return ExecuteGoal
38 */
39function executeAutofixes(registrations, transformPresentation, extractAuthor = exports.NoOpExtractAuthor) {
40 return async (goalInvocation) => {
41 const { id, configuration, goalEvent, credentials, context, progressLog } = goalInvocation;
42 progressLog.write("Evaluating to %d configured autofixes", registrations.length);
43 try {
44 if (registrations.length === 0) {
45 return HandlerResult_1.Success;
46 }
47 const push = goalEvent.push;
48 const appliedAutofixes = [];
49 let editMode;
50 const editResult = await configuration.sdm.projectLoader.doWithProject({
51 credentials,
52 id,
53 context,
54 readOnly: false,
55 cloneOptions: minimalClone_1.minimalClone(push),
56 }, async (project) => {
57 if ((await project.gitStatus()).sha !== id.sha) {
58 return {
59 success: true,
60 edited: false,
61 target: project,
62 description: "Autofixes not executing",
63 phase: "new commit on branch",
64 };
65 }
66 const cri = await createPushImpactListenerInvocation_1.createPushImpactListenerInvocation(goalInvocation, project);
67 if (!!transformPresentation) {
68 editMode = transformPresentation(Object.assign(Object.assign({}, cri), { parameters: {
69 goalInvocation,
70 } }), project);
71 if (editModes_1.isBranchCommit(editMode)) {
72 if (await project.hasBranch(editMode.branch)) {
73 await project.checkout(editMode.branch);
74 }
75 else {
76 await project.createBranch(editMode.branch);
77 }
78 }
79 }
80 const relevantAutofixes = filterImmediateAutofixes(await relevantCodeActions_1.relevantCodeActions(registrations, cri), goalInvocation);
81 progressLog.write("Applying %d relevant autofixes of %d to %s/%s: '%s' of configured '%s'", relevantAutofixes.length, registrations.length, cri.id.owner, cri.id.repo, relevantAutofixes.map(a => a.name).join(", "), registrations.map(a => a.name).join(", "));
82 let cumulativeResult = {
83 target: cri.project,
84 success: true,
85 edited: false,
86 };
87 for (const autofix of _.flatten(relevantAutofixes)) {
88 const thisEdit = await runOne(goalInvocation, cri, autofix, extractAuthor);
89 if (thisEdit.edited) {
90 appliedAutofixes.push(autofix);
91 }
92 cumulativeResult = projectEditorOps_1.combineEditResults(cumulativeResult, thisEdit);
93 }
94 if (cumulativeResult.edited) {
95 await cri.project.push();
96 if (!!editMode && editModes_1.isPullRequest(editMode)) {
97 const targetBranch = editMode.targetBranch || goalEvent.branch;
98 let body = `${editMode.body}
99
100Applied autofixes:
101${appliedAutofixes.map(af => ` * ${slack_messages_1.codeLine(af.name)}`).join("\n")}
102
103[atomist:generated] [atomist:autofix]`.trim();
104 if (editMode.autoMerge) {
105 body = `${body} ${editMode.autoMerge.mode} ${editMode.autoMerge.method ? editMode.autoMerge.method : ""}`.trim();
106 }
107 await cri.project.raisePullRequest(editMode.title, body, targetBranch);
108 }
109 }
110 return cumulativeResult;
111 });
112 if (editResult.edited) {
113 // Send back a stop state to skip downstream goals
114 return {
115 code: 0,
116 message: "Edited",
117 description: goalInvocation.goal.stoppedDescription,
118 state: isNewBranch(editMode, goalEvent.branch) ? types_1.SdmGoalState.success : types_1.SdmGoalState.stopped,
119 phase: detailMessage(appliedAutofixes),
120 };
121 }
122 return {
123 code: 0,
124 description: editResult.description,
125 };
126 }
127 catch (err) {
128 logger_1.logger.warn("Autofixes failed with '%s':\n%s", err.message, err.stack);
129 progressLog.write("Autofixes failed with '%s'", err.message);
130 if (err.stdout) {
131 progressLog.write(err.stdout);
132 }
133 if (err.stderr) {
134 progressLog.write(err.stderr);
135 }
136 return {
137 code: 1,
138 message: err.message,
139 };
140 }
141 };
142}
143exports.executeAutofixes = executeAutofixes;
144/**
145 * Check if this autofix is going to commit to a new branch
146 */
147function isNewBranch(editMode, branch) {
148 if (!!editMode && editModes_1.isBranchCommit(editMode)) {
149 return editMode.branch !== branch;
150 }
151 return false;
152}
153function detailMessage(appliedAutofixes) {
154 // We show only two autofixes by name here as otherwise the message is going to get too long
155 if (appliedAutofixes.length <= 2) {
156 return `${appliedAutofixes.map(af => af.name).join(", ")}`;
157 }
158 else {
159 return `${appliedAutofixes.length} autofixes`;
160 }
161}
162async function runOne(gi, cri, autofix, extractAuthor) {
163 const { progressLog, configuration } = gi;
164 const project = cri.project;
165 progressLog.write("About to transform %s with autofix '%s'", project.id.url, autofix.name);
166 try {
167 const arg2 = Object.assign(Object.assign(Object.assign({}, cri.context), cri), { push: cri, progressLog });
168 const tentativeEditResult = await handlerRegistrations_1.toScalarProjectEditor(autofix.transform, configuration.sdm)(project, arg2, autofix.parametersInstance);
169 const editResult = await confirmEditedness_1.confirmEditedness(tentativeEditResult);
170 if (!editResult.success) {
171 await project.revert();
172 logger_1.logger.warn("Edited %s with autofix %s and success=false, edited=%d", project.id.url, autofix.name, editResult.edited);
173 progressLog.write("Edited %s with autofix %s and success=false, edited=%d", project.id.url, autofix.name, editResult.edited);
174 if (!!autofix.options && autofix.options.ignoreFailure) {
175 // Say we didn't edit and can keep going
176 return { target: project, edited: false, success: false };
177 }
178 }
179 else if (editResult.edited) {
180 progressLog.write("Autofix '%s' made changes", autofix.name);
181 await project.commit(generateCommitMessageForAutofix(autofix));
182 const author = await extractAuthor(gi);
183 if (!!author && !!author.name && !!author.email) {
184 await child_process_1.spawnLog("git", ["commit", "--amend", `--author="${author.name} <${author.email}>"`, "--no-edit"], {
185 cwd: project.baseDir,
186 log: progressLog,
187 });
188 }
189 }
190 else {
191 progressLog.write("Autofix '%s' made no changes", autofix.name);
192 logger_1.logger.debug("No changes were made by autofix %s", autofix.name);
193 }
194 return editResult;
195 }
196 catch (err) {
197 if (!autofix.options || !autofix.options.ignoreFailure) {
198 throw err;
199 }
200 await project.revert();
201 logger_1.logger.warn("Ignoring autofix failure %s on %s with autofix %s", err.message, project.id.url, autofix.name);
202 progressLog.write("Ignoring autofix failure %s on %s with autofix %s", err.message, project.id.url, autofix.name);
203 return { target: project, success: false, edited: false };
204 }
205}
206/**
207 * Filter any provided autofixes whose results were included in the commits of the current push.
208 * @param {AutofixRegistration[]} autofixes
209 * @param {GoalInvocation} gi
210 * @returns {AutofixRegistration[]}
211 */
212function filterImmediateAutofixes(autofixes, gi) {
213 return autofixes.filter(af => !(gi.goalEvent.push.commits || [])
214 .some(c => c.message === generateCommitMessageForAutofix(af)));
215}
216exports.filterImmediateAutofixes = filterImmediateAutofixes;
217/**
218 * Generate a commit message for the provided autofix.
219 * @param {AutofixRegistration} autofix
220 * @returns {string}
221 */
222function generateCommitMessageForAutofix(autofix) {
223 const name = autofix.name.toLowerCase().replace(/ /g, "_");
224 return `Autofix: ${autofix.name}\n\n[atomist:generated] [atomist:autofix=${name}]`;
225}
226exports.generateCommitMessageForAutofix = generateCommitMessageForAutofix;
227exports.AutofixProgressTests = [{
228 test: /About to transform .* autofix '(.*)'/i,
229 phase: "$1",
230 }];
231/**
232 * Default ReportProgress for running autofixes
233 */
234exports.AutofixProgressReporter = progress_1.testProgressReporter(...exports.AutofixProgressTests);
235const NoOpExtractAuthor = async () => {
236 return undefined;
237};
238exports.NoOpExtractAuthor = NoOpExtractAuthor;
239const DefaultExtractAuthor = async (gi) => {
240 const { goalEvent } = gi;
241 const name = _.get(goalEvent, "push.after.author.name");
242 const email = _.get(goalEvent, "push.after.author.emails[0].address");
243 if (!!name && !!email) {
244 return {
245 name,
246 email,
247 };
248 }
249 else {
250 return undefined;
251 }
252};
253exports.DefaultExtractAuthor = DefaultExtractAuthor;
254//# sourceMappingURL=executeAutofixes.js.map
\No newline at end of file