メールでJenkinsにコマンドを送りたい(4) 〜Trigger編〜
コマンドメールの送信、および結果のメール受信までたどり着きました。
実際に使用してみると、これはこれで問題なく動作するのですが、ずっと動かしているとビルド履歴が大変なことに。。。
SCMのポーリング機能を思い出しました。
SCMのポーリング機能は、SCMに変更があるかどうかをまず確認し、変更がなければビルドを実行しない仕様になっています。
つまり、余計なビルド履歴が残りません。この仕様(実装も)を流用できないかと考えました。
基本仕様はSCMポーリングそのままに、CLIメールの到着だけ確認して到着してなければMailCommanderを実行しないようにします。
また、ポーリングのログも見たいと思いましたので、Actionも拡張することにします。
参考にするソースやリソースは、hudson.triggers.SCMTriggerです。
以下がconfig.jellyとなります。SCMTriggerで使用しているconfigと同じにしました。
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <f:entry title="Schedule" field="spec" help="/descriptor/hudson.triggers.TimerTrigger/help/spec"> <f:textarea /> </f:entry> </j:jelly>
以下がhelp_ja.htmlです。
<div> CLIコマンドの書かれた電子メールの到着を監視します。 </div>
以下がindex.jellyとなります。プロジェクトのトップページを表示したときに、左側のメニューに表示させるための設定となります。
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <l:layout> <st:include it="${it.owner}" page="sidepanel.jelly" /> <l:main-panel> <h1>${%title(it.displayName)}</h1> <j:set var="log" value="${it.log}" /> <j:choose> <j:when test="${empty(log)}"> ${%Polling has not run yet.} </j:when> <j:otherwise> <pre> <st:getOutput var="output" /> <j:whitespace>${it.writeLogTo(output)}</j:whitespace> </pre> </j:otherwise> </j:choose> </l:main-panel> </l:layout> </j:jelly>
上のリソースが加わって、以下の図のように、左側のメニューの設定の下に表示されます。
以下が実装になります。ちょっと長いのですが、MailCommanderTriggerのrunメソッド以外は、ほとんどSCMTriggerの内容と同じです。
MailCommandTriggerのrunメソッドでやっていることは、主に3つです。
1)MailCommandBuilderの設定画面で入力されたPOP3サーバの設定をとってきています。
方法が分からなかったので、設定ファイルを直読みしています(←多分よくないだろうな。。。)
2)取得した設定で、メールサーバに問い合わせします。
CLIの書かれたメールがあれば、
if (isCommandExist) job.scheduleBuild(0, new MailCommandTriggerCause());
のように、ビルドを即時にスケジュールします。
3)最後に、ポーリングログを吐き出しています。
public class MailCommandTrigger extends Trigger<SCMedItem> { private static final Logger LOGGER = Logger.getLogger(MailCommandTrigger.class.getName()); @DataBoundConstructor public MailCommandTrigger(String spec) throws ANTLRException { super(spec); } @Override public Collection<? extends Action> getProjectActions() { return Collections.singleton(new MailCommandAction()); } public File getLogFile() { return new File(job.getRootDir(), "mailcommander-polling.log"); } @SuppressWarnings("unchecked") @Override public void run() { String address = null, port = null, username = null, password = null; boolean isCommandExist = false; try { FileInputStream fis = new FileInputStream(new File(job.getRootDir(), "config.xml")); Document dom = new SAXReader().read(fis); Element mailcommander = dom.getRootElement().element("builders").element(MailCommandBuilder.class.getName()); address = mailcommander.elementText("address"); port = mailcommander.elementText("port"); username = mailcommander.elementText("username"); password = mailcommander.elementText("password"); fis.close(); } catch (IOException ioe) { ioe.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); } Properties props = new Properties(); Session sess = Session.getDefaultInstance(props); Store store; try { store = sess.getStore("pop3"); } catch (NoSuchProviderException e) { e.printStackTrace(); return; } StringBuffer commands = new StringBuffer(); for (CLICommand c : CLICommand.all()) commands.append(c.getName()); try { store.connect(address, Integer.valueOf(port), username, password); Folder rootFolder = store.getDefaultFolder(); Folder inbox = rootFolder.getFolder("INBOX"); inbox.open(Folder.READ_ONLY); Message[] messages = null; if (inbox.getMessageCount() > 10) messages = inbox.getMessages(inbox.getMessageCount() - 10 + 1, inbox.getMessageCount()); else messages = inbox.getMessages(); FetchProfile fp = new FetchProfile(); fp.add("Subject"); inbox.fetch(messages, fp); for (Message message : messages) { String findStr = message.getSubject().split(" ")[0]; if (commands.indexOf(findStr) != -1) { isCommandExist = true; break; } } inbox.close(true); store.close(); } catch (MessagingException e) { e.printStackTrace(); return; } if (isCommandExist) job.scheduleBuild(0, new MailCommandTriggerCause()); try { StreamTaskListener listener = new StreamTaskListener(getLogFile()); try { PrintStream logger = listener.getLogger(); logger.println("Started on " + DateFormat.getDateTimeInstance().format(new Date())); if (isCommandExist) logger.println("Changes found"); else logger.println("No changes"); } catch (Error e) { e.printStackTrace(listener.error("Failed to record Mail Commander polling")); LOGGER.log(Level.SEVERE, "Failed to record SCM polling", e); throw e; } catch (RuntimeException e) { e.printStackTrace(listener.error("Failed to record Mail Commander polling")); LOGGER.log(Level.SEVERE, "Failed to record Mail Commander polling", e); throw e; } finally { listener.close(); } } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failed to record Mail Commander polling", e); } } @Extension public static class DescriptorImpl extends TriggerDescriptor { public boolean isApplicable(Item item) { return item instanceof BuildableItem; } public String getDisplayName() { return Messages.MailCommandTrigger_DisplayName(); } public FormValidation doCheck(@QueryParameter String value) { return doCheckSpec(value); } public FormValidation doCheckSpec(@QueryParameter String value) { try { String msg = CronTabList.create(fixNull(value)).checkSanity(); if (msg != null) return FormValidation.warning(msg); return FormValidation.ok(); } catch (ANTLRException e) { return FormValidation.error(e.getMessage()); } } } public static class MailCommandTriggerCause extends Cause { @Override public String getShortDescription() { return "Mail Commander Trigger"; } @Override public boolean equals(Object o) { return o instanceof MailCommandTriggerCause; } @Override public int hashCode() { return 5; } } public final class MailCommandAction implements Action { public AbstractProject<?, ?> getOwner() { return job.asProject(); } public String getIconFileName() { return "clipboard.gif"; } public String getDisplayName() { return "Mail Command Action"; } public String getUrlName() { return "mailcommandPollLog"; } public String getLog() throws IOException { return Util.loadFile(getLogFile()); } public void writeLogTo(XMLOutput out) throws IOException { new AnnotatedLargeText<MailCommandAction>(getLogFile(), Charset.defaultCharset(), true, this).writeHtmlTo(0,out.asWriter()); } } }
と、ここまで作成してきて、ふと疑問。
今回の要件のような、BuilderやTrigger、Publisherのように単独ではあまり意味をなさないようなプラグインって
1つのクラスで実装して、TriggerやPublisherはBuilderの内部クラスにまとめてしまったほうが良いのでしょうか。
ともあれ、この実装により定期実行をずっとしていてもビルド履歴が無駄に溜まる心配がなくなりました。
最後に、Builderを少し改良してgroovyシェルを実行できるようにしたいと思います。