-
Notifications
You must be signed in to change notification settings - Fork 401
/
Copy pathGitHubPushTrigger.java
480 lines (421 loc) · 16 KB
/
GitHubPushTrigger.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
package com.cloudbees.jenkins;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
import hudson.Util;
import hudson.XmlFile;
import hudson.console.AnnotatedLargeText;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Item;
import hudson.model.Job;
import hudson.model.Project;
import hudson.triggers.SCMTrigger;
import hudson.triggers.Trigger;
import hudson.triggers.TriggerDescriptor;
import hudson.util.FormValidation;
import hudson.util.NamingThreadFactory;
import hudson.util.SequentialExecutionQueue;
import hudson.util.StreamTaskListener;
import jenkins.model.Jenkins;
import jenkins.model.ParameterizedJobMixIn;
import jenkins.scm.api.SCMEvent;
import jenkins.triggers.SCMTriggerItem;
import jenkins.triggers.SCMTriggerItem.SCMTriggerItems;
import org.apache.commons.jelly.XMLOutput;
import org.jenkinsci.plugins.github.GitHubPlugin;
import org.jenkinsci.plugins.github.admin.GitHubHookRegisterProblemMonitor;
import org.jenkinsci.plugins.github.config.GitHubPluginConfig;
import org.jenkinsci.plugins.github.internal.GHPluginConfigException;
import org.jenkinsci.plugins.github.migration.Migrator;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.Stapler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import edu.umd.cs.findbugs.annotations.NonNull;
import jakarta.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.commons.lang3.Validate.notNull;
import static org.jenkinsci.plugins.github.util.JobInfoHelpers.asParameterizedJobMixIn;
/**
* Triggers a build when we receive a GitHub post-commit webhook.
*
* @author Kohsuke Kawaguchi
*/
public class GitHubPushTrigger extends Trigger<Job<?, ?>> implements GitHubTrigger {
@DataBoundConstructor
public GitHubPushTrigger() {
}
/**
* Called when a POST is made.
*/
@Deprecated
public void onPost() {
onPost(GitHubTriggerEvent.create()
.build()
);
}
/**
* Called when a POST is made.
*/
public void onPost(String triggeredByUser, String triggeredByRef) {
onPost(GitHubTriggerEvent.create()
.withOrigin(SCMEvent.originOf(Stapler.getCurrentRequest2()))
.withTriggeredByUser(triggeredByUser)
.withTriggeredByRef(triggeredByRef)
.build()
);
}
/**
* Called when a POST is made.
*/
public void onPost(final GitHubTriggerEvent event) {
if (Objects.isNull(job)) {
return; // nothing to do
}
Job<?, ?> currentJob = notNull(job, "Job can't be null");
final String pushBy = event.getTriggeredByUser();
final String ref = event.getTriggeredByRef();
DescriptorImpl d = getDescriptor();
d.checkThreadPoolSizeAndUpdateIfNecessary();
d.queue.execute(new Runnable() {
private boolean runPolling() {
try {
StreamTaskListener listener = new StreamTaskListener(getLogFileForJob(currentJob));
try {
PrintStream logger = listener.getLogger();
long start = System.currentTimeMillis();
logger.println("Started on " + DateFormat.getDateTimeInstance().format(new Date()));
if (event.getOrigin() != null) {
logger.format("Started by event from %s on %tc%n", event.getOrigin(), event.getTimestamp());
}
SCMTriggerItem item = SCMTriggerItems.asSCMTriggerItem(currentJob);
if (null == item) {
throw new IllegalStateException("Job is not an SCMTriggerItem: " + currentJob);
}
boolean result = item.poll(listener).hasChanges();
logger.println("Done. Took " + Util.getTimeSpanString(System.currentTimeMillis() - start));
if (result) {
logger.println("Changes found");
} else {
logger.println("No changes");
}
return result;
} catch (Error e) {
e.printStackTrace(listener.error("Failed to record SCM polling"));
LOGGER.error("Failed to record SCM polling", e);
throw e;
} catch (RuntimeException e) {
e.printStackTrace(listener.error("Failed to record SCM polling"));
LOGGER.error("Failed to record SCM polling", e);
throw e;
} finally {
listener.close();
}
} catch (IOException e) {
LOGGER.error("Failed to record SCM polling", e);
}
return false;
}
public void run() {
if (runPolling()) {
GitHubPushCause cause;
try {
cause = new GitHubPushCause(getLogFileForJob(currentJob), pushBy, ref);
} catch (IOException e) {
LOGGER.warn("Failed to parse the polling log", e);
cause = new GitHubPushCause(pushBy, ref);
}
if (asParameterizedJobMixIn(currentJob).scheduleBuild(cause)) {
LOGGER.info("SCM changes detected in " + currentJob.getFullName()
+ ". Triggering #" + currentJob.getNextBuildNumber());
} else {
LOGGER.info("SCM changes detected in " + currentJob.getFullName()
+ ". Job is already in the queue");
}
}
}
});
}
/**
* Returns the file that records the last/current polling activity.
*/
public File getLogFile() {
try {
return getLogFileForJob(notNull(job, "Job can't be null!"));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
/**
* Returns the file that records the last/current polling activity.
*/
private File getLogFileForJob(@NonNull Job job) throws IOException {
return new File(job.getRootDir(), "github-polling.log");
}
/**
* @deprecated Use {@link GitHubRepositoryNameContributor#parseAssociatedNames(AbstractProject)}
*/
@Deprecated
public Set<GitHubRepositoryName> getGitHubRepositories() {
return Collections.emptySet();
}
/**
* @deprecated
* Use {@link GitHubRepositoryNameContributor#parseAssociatedBranches(AbstractProject)}
*/
public Set<GitHubBranch> getGitHubBranches() {
return Collections.emptySet();
}
@Override
public void start(Job<?, ?> project, boolean newInstance) {
super.start(project, newInstance);
if (newInstance && GitHubPlugin.configuration().isManageHooks()) {
registerHooks();
}
}
/**
* Tries to register hook for current associated job.
* Do this lazily to avoid blocking the UI thread.
* Useful for using from groovy scripts.
*
* @since 1.11.2
*/
public void registerHooks() {
GitHubWebHook.get().registerHookFor(job);
}
@Override
public void stop() {
if (job == null) {
return;
}
if (GitHubPlugin.configuration().isManageHooks()) {
Cleaner cleaner = Cleaner.get();
if (cleaner != null) {
cleaner.onStop(job);
}
}
}
@Override
public Collection<? extends Action> getProjectActions() {
if (job == null) {
return Collections.emptyList();
}
return Collections.singleton(new GitHubWebHookPollingAction());
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
/**
* Action object for {@link Project}. Used to display the polling log.
*/
public final class GitHubWebHookPollingAction implements Action {
public Job<?, ?> getOwner() {
return job;
}
public String getIconFileName() {
return "clipboard.png";
}
public String getDisplayName() {
return "GitHub Hook Log";
}
public String getUrlName() {
return "GitHubPollLog";
}
public String getLog() throws IOException {
return Util.loadFile(getLogFileForJob(Objects.requireNonNull(job)));
}
/**
* Writes the annotated log to the given output.
*
* @since 1.350
*/
@SuppressFBWarnings(
value = "RV_RETURN_VALUE_IGNORED",
justification =
"method signature does not permit plumbing through the return value")
public void writeLogTo(XMLOutput out) throws IOException {
new AnnotatedLargeText<GitHubWebHookPollingAction>(
getLogFileForJob(Objects.requireNonNull(job)),
Charsets.UTF_8,
true,
this)
.writeHtmlTo(0, out.asWriter());
}
}
@Extension
@Symbol("githubPush")
public static class DescriptorImpl extends TriggerDescriptor {
private final transient SequentialExecutionQueue queue =
new SequentialExecutionQueue(Executors.newSingleThreadExecutor(threadFactory()));
private transient String hookUrl;
private transient List<Credential> credentials;
@Inject
private transient GitHubHookRegisterProblemMonitor monitor;
@Inject
private transient SCMTrigger.DescriptorImpl scmTrigger;
private transient int maximumThreads = Integer.MIN_VALUE;
public DescriptorImpl() {
checkThreadPoolSizeAndUpdateIfNecessary();
}
/**
* Update the {@link java.util.concurrent.ExecutorService} instance.
*/
/*package*/
synchronized void checkThreadPoolSizeAndUpdateIfNecessary() {
if (scmTrigger != null) {
int count = scmTrigger.getPollingThreadCount();
if (maximumThreads != count) {
maximumThreads = count;
queue.setExecutors(
(count == 0
? Executors.newCachedThreadPool(threadFactory())
: Executors.newFixedThreadPool(maximumThreads, threadFactory())));
}
}
}
@Override
public boolean isApplicable(Item item) {
return item instanceof Job && SCMTriggerItems.asSCMTriggerItem(item) != null
&& item instanceof ParameterizedJobMixIn.ParameterizedJob;
}
@Override
public String getDisplayName() {
return "GitHub hook trigger for GITScm polling";
}
/**
* True if Jenkins should auto-manage hooks.
*
* @deprecated Use {@link GitHubPluginConfig#isManageHooks()} instead
*/
@Deprecated
public boolean isManageHook() {
return GitHubPlugin.configuration().isManageHooks();
}
/**
* Returns the URL that GitHub should post.
*
* @deprecated use {@link GitHubPluginConfig#getHookUrl()} instead
*/
@Deprecated
public URL getHookUrl() throws GHPluginConfigException {
return GitHubPlugin.configuration().getHookUrl();
}
/**
* @return null after migration
* @deprecated use {@link GitHubPluginConfig#getConfigs()} instead.
*/
@Deprecated
public List<Credential> getCredentials() {
return credentials;
}
/**
* Used only for migration
*
* @return null after migration
* @deprecated use {@link GitHubPluginConfig#getHookUrl()}
*/
@Deprecated
public URL getDeprecatedHookUrl() {
if (isEmpty(hookUrl)) {
return null;
}
try {
return new URL(hookUrl);
} catch (MalformedURLException e) {
LOGGER.warn("Malformed hook url skipped while migration ({})", e.getMessage());
return null;
}
}
/**
* Used to cleanup after migration
*/
public void clearDeprecatedHookUrl() {
this.hookUrl = null;
}
/**
* Used to cleanup after migration
*/
public void clearCredentials() {
this.credentials = null;
}
/**
* @deprecated use {@link GitHubPluginConfig#isOverrideHookUrl()}
*/
@Deprecated
public boolean hasOverrideURL() {
return GitHubPlugin.configuration().isOverrideHookUrl();
}
/**
* Uses global xstream to enable migration alias used in
* {@link Migrator#enableCompatibilityAliases()}
*/
@Override
protected XmlFile getConfigFile() {
return new XmlFile(Jenkins.XSTREAM2, super.getConfigFile().getFile());
}
public static DescriptorImpl get() {
return Trigger.all().get(DescriptorImpl.class);
}
public static boolean allowsHookUrlOverride() {
return ALLOW_HOOKURL_OVERRIDE;
}
private static ThreadFactory threadFactory() {
return new NamingThreadFactory(Executors.defaultThreadFactory(), "GitHubPushTrigger");
}
/**
* Checks that repo defined in this item is not in administrative monitor as failed to be registered.
* If that so, shows warning with some instructions
*
* @param item - to check against. Should be not null and have at least one repo defined
*
* @return warning or empty string
* @since 1.17.0
*/
@SuppressWarnings("unused")
@Restricted(NoExternalUse.class) // invoked from Stapler
public FormValidation doCheckHookRegistered(@AncestorInPath Item item) {
Preconditions.checkNotNull(item, "Item can't be null if wants to check hook in monitor");
if (!item.hasPermission(Item.CONFIGURE)) {
return FormValidation.ok();
}
Collection<GitHubRepositoryName> repos = GitHubRepositoryNameContributor.parseAssociatedNames(item);
for (GitHubRepositoryName repo : repos) {
if (monitor.isProblemWith(repo)) {
return FormValidation.warning(
org.jenkinsci.plugins.github.Messages.github_trigger_check_method_warning_details(
repo.getUserName(), repo.getRepositoryName(), repo.getHost()
));
}
}
return FormValidation.ok();
}
}
/**
* Set to false to prevent the user from overriding the hook URL.
*/
public static final boolean ALLOW_HOOKURL_OVERRIDE = !Boolean.getBoolean(
GitHubPushTrigger.class.getName() + ".disableOverride"
);
private static final Logger LOGGER = LoggerFactory.getLogger(GitHubPushTrigger.class);
}