package ru.yandex.jenkins.plugins.debuilder; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.BuildBadgeAction; import hudson.model.BuildListener; import hudson.model.Result; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Descriptor; import hudson.model.Project; import hudson.plugins.git.GitSCM; import hudson.scm.SCM; import hudson.scm.SubversionSCM; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import hudson.util.ComboBoxModel; import hudson.util.DescribableList; import hudson.util.FormValidation; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.io.Serializable; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nonnull; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.apache.commons.lang3.text.StrSubstitutor; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.tmatesoft.svn.core.auth.ISVNAuthenticationProvider; import ru.yandex.jenkins.plugins.debuilder.DebUtils.Runner; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; public class DebianPackagePublisher extends Recorder implements Serializable { private static final long serialVersionUID = 1L; private static final String PREFIX = "debian-package-publisher"; private String repoId; private String commitMessage; private final boolean commitChanges; @DataBoundConstructor public DebianPackagePublisher(String repoId, String commitMessage, boolean commitChanges) { this.commitChanges = commitChanges; this.commitMessage = commitMessage; this.repoId = repoId; } private DebianPackageRepo getRepo(AbstractBuild<?, ?> build, Runner runner) throws IOException, InterruptedException { String expandedRepo = build.getEnvironment(runner.getListener()).expand(repoId); for(DebianPackageRepo repo: getDescriptor().getRepositories()) { if (repo.getName().equals(expandedRepo)) { return repo; } } throw new IllegalArgumentException(MessageFormat.format("Repo {0} is not found in global configuration", expandedRepo)); } @SuppressWarnings({ "rawtypes", "unchecked" }) public static String getUsedCommitMessage(AbstractBuild build) { DescribableList<Publisher, Descriptor<Publisher>> publishersList = ((Project)build.getProject()).getPublishersList(); for (Publisher publisher: publishersList) { if (publisher instanceof DebianPackagePublisher) { return ((DebianPackagePublisher) publisher).commitMessage; } } return ""; } private FilePath getRemoteKeyPath(AbstractBuild<?, ?> build, Runner runner) throws IOException, InterruptedException { String keysDir = "debian-package-builder-keys"; String relativeKeyPath = new File(keysDir, getRepo(build, runner).getKeypath()).getPath(); File absoluteKeyPath = new File (Jenkins.getInstance().getRootDir(), relativeKeyPath); FilePath localKey = new FilePath(absoluteKeyPath); FilePath remoteKey = build.getWorkspace().createTextTempFile("private", "key", localKey.readToString()); remoteKey.chmod(0600); return remoteKey; } private FilePath[] generateDuploadConf(AbstractBuild<?, ?> build, Runner runner) throws IOException, InterruptedException, DebianizingException { String confTemplate = "package config;\n\n" + "$default_host = '${name}';\n\n" + "$cfg{'${name}'} = {\n" + "\tlogin => '${login}',\n" + "\tfqdn => '${fqdn}',\n" + "\tmethod => '${method}',\n" + "\tincoming => '${incoming}',\n" + "\tdinstall_runs => 0,\n" + "\toptions => '${options}',\n" + "};\n\n" + "1;\n"; Map<String, String> values = new HashMap<String, String>(); DebianPackageRepo repo = getRepo(build, runner); FilePath keyPath = getRemoteKeyPath(build, runner); values.put("name", repo.getName()); values.put("method", repo.getMethod()); values.put("fqdn", repo.getFqdn()); values.put("incoming", repo.getIncoming()); values.put("login", repo.getLogin()); values.put("options", MessageFormat.format("-i {0} ", keyPath.getRemote()) + repo.getOptions()); StrSubstitutor substitutor = new StrSubstitutor(values); String conf = substitutor.replace(confTemplate); FilePath duploadConf = build.getWorkspace().createTempFile("dupload", "conf"); duploadConf.touch(System.currentTimeMillis()/1000); duploadConf.write(conf, "UTF-8"); return new FilePath[] { duploadConf, keyPath }; } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws IOException { PrintStream logger = listener.getLogger(); if (build.getResult() != null && build.getResult().isWorseThan(Result.SUCCESS)) { logger.println(MessageFormat.format(DebianPackageBuilder.ABORT_MESSAGE, PREFIX, "Build is not success, will not execute debrelease")); return true; } Runner runner = new DebUtils.Runner(build, launcher, listener, PREFIX); FilePath[] tempFiles = null; try { runner.runCommand("sudo apt-get -y install dupload devscripts"); tempFiles = generateDuploadConf(build, runner); String duploadConf = tempFiles[0].getRemote(); List<String> builtModules = new ArrayList<String>(); for (BuildBadgeAction action: build.getBadgeActions()) { if (action instanceof DebianBadge) { builtModules.add(((DebianBadge) action).getModule()); } } boolean wereBuilds = false; for (String module: DebianPackageBuilder.getRemoteModules(build, runner)) { if (! builtModules.contains(new FilePath(build.getWorkspace().getChannel(), module).child("debian").getRemote())) { runner.announce("Module in {0} was not built - not releasing", module); continue; } if (!runner.runCommandForResult("cd ''{0}'' && cp ''{1}'' dupload.conf && trap ''rm -f dupload.conf'' EXIT && debrelease -c", module, duploadConf)) { throw new DebianizingException("Debrelease failed"); } wereBuilds = true; } if (wereBuilds && commitChanges) { String expandedCommitMessage = getExpandedCommitMessage(build, listener); commitChanges(build, runner, expandedCommitMessage); } } catch (InterruptedException e) { logger.println(MessageFormat.format(DebianPackageBuilder.ABORT_MESSAGE, PREFIX, e.getMessage())); build.setResult(Result.UNSTABLE); } catch (DebianizingException e) { logger.println(MessageFormat.format(DebianPackageBuilder.ABORT_MESSAGE, PREFIX, e.getMessage())); build.setResult(Result.UNSTABLE); } finally { if (tempFiles != null) { for (FilePath tempFile : tempFiles) { try { tempFile.delete(); } catch (InterruptedException e) { logger.println(MessageFormat.format("[{0}] Error deleting {1}: {2}", PREFIX, tempFile.getRemote(), e.getMessage())); } } } } return true; } private String getExpandedCommitMessage(AbstractBuild<?, ?> build, BuildListener listener) throws IOException, InterruptedException { EnvVars env = build.getEnvironment(listener); return env.expand(getCommitMessage()); } private void commitChanges(AbstractBuild<?, ?> build, Runner runner, String commitMessage) throws DebianizingException, IOException, InterruptedException { SCM scm = build.getProject().getScm(); if (scm instanceof SubversionSCM) { commitToSVN(build, runner, (SubversionSCM)scm, commitMessage); } else if (scm instanceof GitSCM) { commitToGitAndPush(build, runner, (GitSCM)scm, commitMessage); } else { throw new DebianizingException("SCM used is not a know one but " + scm.getType()); } } private void commitToGitAndPush(final AbstractBuild<?, ?> build, final Runner runner, GitSCM scm, String commitMessage) throws DebianizingException { try { GitCommitHelper helper = new GitCommitHelper(build, scm, runner, commitMessage, DebianPackageBuilder.getRemoteModules(build, runner)); if (build.getWorkspace().act(helper)) { runner.announce("Successfully commited to git"); } else { throw new DebianizingException("Failed to commit to git"); } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } private void commitToSVN(final AbstractBuild<?, ?> build, final Runner runner, SubversionSCM svn, String commitMessage) throws DebianizingException { try { for (String module: DebianPackageBuilder.getRemoteModules(build, runner)) { ISVNAuthenticationProvider authenticationProvider = svn.createAuthenticationProvider(build.getProject(), ChangesExtractor.findOurLocation(build, svn, runner, module)); SVNCommitHelper helper = new SVNCommitHelper(authenticationProvider, module, commitMessage); runner.announce("Commited revision <{0}> of <{2}> with message <{1}>", runner.getChannel().call(helper), commitMessage, module); } } catch (IOException e) { e.printStackTrace(); throw new DebianizingException("IOException: " + e.getMessage(), e); } catch (InterruptedException e) { e.printStackTrace(); throw new DebianizingException("Interrupted: " + e.getMessage(), e); } } @Override public boolean needsToRunAfterFinalized() { return false; } @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } @Extension public static class DescriptorImpl extends BuildStepDescriptor<Publisher> { private List<DebianPackageRepo> repos = new ArrayList<DebianPackageRepo>(); public DescriptorImpl() { super(); load(); } public ArrayList<DebianPackageRepo> getRepositories() { return new ArrayList<DebianPackageRepo>(repos); } public ComboBoxModel doFillRepoIdItems() { ComboBoxModel model = new ComboBoxModel(); for (DebianPackageRepo repo: repos) { model.add(repo.getName()); } return model; } public FormValidation doCheckRepoId(@Nonnull @QueryParameter final String repoId) { if (repoId.contains("$")) { // poor man's check that it has a parameter return FormValidation.warning("The actual repository will be determined at run-time, take care"); } else if ( Collections2.filter( getRepositories(), new Predicate<DebianPackageRepo>() { @Override public boolean apply(DebianPackageRepo repo) { return repoId.equals(repo.getName()); } } ).size() == 0) { return FormValidation.error("There is no such repository configured"); } else { return FormValidation.ok(); } } public FormValidation doCheckMethod(@QueryParameter String method) { if (method != "scpb") { return FormValidation.error("This method is not supported yet"); } else { return FormValidation.ok(); } } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { repos = req.bindJSONToList(DebianPackageRepo.class, formData.get("repositories")); save(); return super.configure(req,formData); } @SuppressWarnings("rawtypes") @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } @Override public String getDisplayName() { return "Publish debian packages"; } } public boolean isCommitChanges() { return commitChanges; } public String getCommitMessage() { return commitMessage; } public String getRepoId() { return repoId; } }