package com.cloudbees.jenkins; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; 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.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 javax.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.Set; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import static org.apache.commons.lang3.StringUtils.isEmpty; 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) { onPost(GitHubTriggerEvent.create() .withOrigin(SCMEvent.originOf(Stapler.getCurrentRequest())) .withTriggeredByUser(triggeredByUser) .build() ); } /** * Called when a POST is made. */ public void onPost(final GitHubTriggerEvent event) { final String pushBy = event.getTriggeredByUser(); DescriptorImpl d = getDescriptor(); d.checkThreadPoolSizeAndUpdateIfNecessary(); d.queue.execute(new Runnable() { private boolean runPolling() { try { StreamTaskListener listener = new StreamTaskListener(getLogFile()); 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()); } boolean result = SCMTriggerItems.asSCMTriggerItem(job).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(getLogFile(), pushBy); } catch (IOException e) { LOGGER.warn("Failed to parse the polling log", e); cause = new GitHubPushCause(pushBy); } if (asParameterizedJobMixIn(job).scheduleBuild(cause)) { LOGGER.info("SCM changes detected in " + job.getFullName() + ". Triggering #" + job.getNextBuildNumber()); } else { LOGGER.info("SCM changes detected in " + job.getFullName() + ". Job is already in the queue"); } } } }); } /** * Returns the file that records the last/current polling activity. */ public File getLogFile() { return new File(job.getRootDir(), "github-polling.log"); } /** * @deprecated Use {@link GitHubRepositoryNameContributor#parseAssociatedNames(AbstractProject)} */ @Deprecated public Set<GitHubRepositoryName> getGitHubRepositories() { 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(getLogFile()); } /** * Writes the annotated log to the given output. * * @since 1.350 */ public void writeLogTo(XMLOutput out) throws IOException { new AnnotatedLargeText<GitHubWebHookPollingAction>(getLogFile(), 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"); 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); }