<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
	
	<title type="text" xml:lang="en"></title>
	<link type="application/atom+xml" href="http://www.paulzumbrun.com/atom.xml" rel="self"/>
 	<link type="text" href="http://www.paulzumbrun.com" rel="alternate"/>
	<updated>2015-05-12T00:18:02-05:00</updated>
	<id>http://www.paulzumbrun.com</id>
	<author>
		<name>Paul Zumbrun</name>
	</author>
	<rights>Copyright (c) 2012 - 2015, Paul Zumbrun. All rights reserved.</rights>
	
	
	
	
	
	<entry>
		<title>Fixing up our Atlassian suite.</title>
		<link href="http://www.paulzumbrun.com/Atlassian-suite/"/>
		<updated>2013-08-13T00:00:00-05:00</updated>
		<id>http://www.paulzumbrun.com/Atlassian-suite</id>
		<content type="html">&lt;p&gt;At &lt;a href=&quot;http://www.beaconfire.com&quot;&gt;the consulting firm I work at&lt;/a&gt; we use a handful of pieces of the Atlassian suite: JIRA, FishEye, HipChat.&lt;/p&gt;

&lt;p&gt;HipChat we&#39;ve only just started using. It&#39;s exciting for me since we didn&#39;t have a good group chat solution prior to this. As a long-time IRC junkie, I jumped on the idea of having &lt;a href=&quot;http://hubot.github.com/&quot;&gt;bots&lt;/a&gt; and in-channel notifications and such. Admittedly a lot of this kind of stuff can be of no true value beyond it being kind of fun, but there were a couple interactions that ended up being of value. The highest priority HipChat integration for me was getting Git announcing pushes to our HipChat rooms.&lt;/p&gt;

&lt;p&gt;Rather than using the managed Git repository features of FishEye (or something like Gitorious, Stash, BitBucket, GitHub), we have our own central Git repository called vm-repos. It&#39;s just a collection of bare repositories accessible via SSH which serves as the origin for most of our private code. Nothing fancy, but as a consulting firm we have lots of different projects, and lots of different repos.&lt;/p&gt;

&lt;p&gt;Something we had been wanting to set up for a while was getting FishEye to automatically pull in new Git repositories that had been added to vm-repos. We mostly use FishEye as a code search utility, so getting repos added was less crucial than if we were using the full Crucible/FishEye setup for code reviews and commenting. Manually adding our projects was tedious, and since it was low priority too, it meant that a lot of projects never ended up in FishEye. This lead to a vicious cycle of FishEye neglect.&lt;/p&gt;

&lt;p&gt;Exacerbating the problem, we don&#39;t have a DevOps team, which means that unless our internal infrastructure gets critically broken, we typically don&#39;t have time to work on it. Our developers are busy working on client work, or more interesting internal stuff (like HipChat bots, apparently).&lt;/p&gt;

&lt;p&gt;Setting up HipChat integrations finally gave me the motivation to execute this.&lt;/p&gt;

&lt;h2&gt;HipChat and Git Integration&lt;/h2&gt;

&lt;p&gt;This ended up being really easy thanks to the efforts of some other folks in the community. I wanted a post-receive hook that would notify HipChat of new commits. I started from a &lt;a href=&quot;https://github.com/eirc/git-hipchat-hook/blob/master/hipchat-post-receive&quot;&gt;HipChat post-receive hook that I found on GitHub&lt;/a&gt;. I dropped this in place and then I just set up a little shell script that installs a post-receive hook in all of our bare repositories that don&#39;t have a hook already:&lt;/p&gt;

&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;find /data/repos/git -name hooks -type d &lt;span class=&quot;se&quot;&gt;\!&lt;/span&gt; -exec &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; -e &lt;span class=&quot;s1&quot;&gt;&#39;{}/post-receive&#39;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    -exec cp ~/serverscripts/vm-repos/post-receive.template &lt;span class=&quot;o&quot;&gt;{}&lt;/span&gt;/post-receive &lt;span class=&quot;se&quot;&gt;\;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;post-receive.template&lt;/code&gt; file is:&lt;/p&gt;

&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;read &lt;/span&gt;oldrev newrev refname
&lt;span class=&quot;k&quot;&gt;do
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$oldrev&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$newrev&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$refname&lt;/span&gt; | /bin/bash ~/serverscripts/vm-repos/notifyhipchat.sh
        &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$oldrev&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$newrev&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$refname&lt;/span&gt; | /bin/bash ~/serverscripts/vm-repos/notifyfisheye.sh

        &lt;span class=&quot;c&quot;&gt;# Other post-receive hooks can go here, following the same pattern.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;The main thing I had to fix with the HipChat post receive hook was that this script only notified single rooms. We typically have one HipChat room &lt;em&gt;per project&lt;/em&gt;, and we didn&#39;t want everything going all to the same place. (Although we did create a room just for push announcements; that&#39;s the &lt;code&gt;$DEFAULT_ROOM&lt;/code&gt; you see below).&lt;/p&gt;

&lt;p&gt;So I updated the script to read from a &lt;code&gt;rooms.conf&lt;/code&gt; file, and depending on the repo that had just been pushed to, announce to different rooms or to multiple rooms:&lt;/p&gt;

&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;c&quot;&gt;# Near the top&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;ROOM_IDS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$DEFAULT_ROOM&lt;/span&gt;,&lt;span class=&quot;k&quot;&gt;$(&lt;/span&gt;grep &lt;span class=&quot;s2&quot;&gt;&quot;^&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REPO&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;=&quot;&lt;/span&gt; rooms.conf | grep -o &lt;span class=&quot;s1&quot;&gt;&#39;[0-9,]\+$&#39;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Then at the bottom of the file:&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;IFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&#39;,&#39;&lt;/span&gt; ; &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;HIPCHAT_ROOM &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$ROOM_IDS&lt;/span&gt;; &lt;span class=&quot;k&quot;&gt;do
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MSG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nv&quot;&gt;$HIPCHAT_SCRIPT&lt;/span&gt; -c &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HIPCHAT_COLOR&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; -t &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HIPCHAT_TOKEN&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; -r &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HIPCHAT_ROOM&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; -f &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HIPCHAT_FROM&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; -n 1 &amp;gt; /dev/null
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;And then rooms.conf is just a simple mapping of repo directories to HipChat room IDs.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;repo1=10001
repo2=10002
repo3=10003,10005
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The next change was FishEye support, so that repo names and changesets linked to FishEye:&lt;/p&gt;

&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;c&quot;&gt;# We have some directory structures which we want to include in the FishEye project names.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Replace / with _ for paths&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;REPOPATH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt; | sed &lt;span class=&quot;s1&quot;&gt;&#39;s/\/data\/repos\/git\///&#39;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;REPONAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REPOPATH&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | sed &lt;span class=&quot;s1&quot;&gt;&#39;s/\//_/g&#39;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# repo links:&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;REPO_LINK&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;a href=&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;http://&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$FISHEYE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/browse/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REPONAME&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REPO&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;lt;/a&amp;gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# changeset links:&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;LOG&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$GIT_EXEC&lt;/span&gt; log &lt;span class=&quot;nv&quot;&gt;$OLDREV&lt;/span&gt;..&lt;span class=&quot;nv&quot;&gt;$NEWREV&lt;/span&gt; --format&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;- %s (&amp;lt;a href=&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;http://&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$FISHEYE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/changelog/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REPO&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;?cs=%H&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;gt;%h&amp;lt;/a&amp;gt;)&quot;&lt;/span&gt;&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;This is what lead into finally fixing our FishEye setup. A lot of these FishEye links being generated were bogus because our repo names in FishEye didn&#39;t line up with our repo names on vm-repos, or in some cases, we just didn&#39;t have the FishEye repos created at all.&lt;/p&gt;

&lt;h2&gt;FishEye Automatic Discovery of Git Repos&lt;/h2&gt;

&lt;p&gt;There are some FishEye plugins out there that theoretically do this, like the &lt;a href=&quot;https://marketplace.atlassian.com/plugins/com.atlassian.fisheye.repositron&quot;&gt;FishEye Directory Scanner&lt;/a&gt;. We&#39;re on FishEye 3.x though, and this plugin hasn&#39;t been updated yet. The REST API for FishEye also doesn&#39;t seem to support the creation of repos. I was poking around at FishEye to try to figure out how to do this. I was about to do this with browser automation using &lt;a href=&quot;http://fluent.stirno.com/blog/FluentAutomation-scriptcs/&quot;&gt;Fluent Automation and ScriptCS&lt;/a&gt;, but noticed that repos are actually defined in plain text in the &lt;code&gt;config.xml&lt;/code&gt; file. I tested it out by adding a repo and sure enough, after restarting FishEye, it picked this up and indexed it right away.&lt;/p&gt;

&lt;p&gt;So I wrote another script that would find all of our Git repositories and rewrite FishEye&#39;s &lt;code&gt;config.xml&lt;/code&gt; to include them. Caveat: I&#39;d never call bash scripting an area of expertise; this is a quick-and-dirty hack and I freely admit that, but it works great!&lt;/p&gt;

&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nv&quot;&gt;XMLFILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/data/appdata/fisheye/config.xml

&lt;span class=&quot;nv&quot;&gt;ALLREPOS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$(&lt;/span&gt;ssh git@vm-repos find /data/repos/git -name &lt;span class=&quot;s2&quot;&gt;&quot;config&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;DATA&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;IFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$&#39;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&#39;; for LINE in $ALLREPOS; do
    # Replace out ugly path bits to just get the name
    REPOPATH=$(echo &quot;$LINE&quot; | sed &#39;&lt;/span&gt;s/&lt;span class=&quot;se&quot;&gt;\/&lt;/span&gt;data&lt;span class=&quot;se&quot;&gt;\/&lt;/span&gt;repos&lt;span class=&quot;se&quot;&gt;\/&lt;/span&gt;git&lt;span class=&quot;se&quot;&gt;\/&lt;/span&gt;//&lt;span class=&quot;s1&quot;&gt;&#39; | sed &#39;&lt;/span&gt;s/&lt;span class=&quot;se&quot;&gt;\/&lt;/span&gt;config&lt;span class=&quot;nv&quot;&gt;$/&lt;/span&gt;/&lt;span class=&quot;s1&quot;&gt;&#39;)

    # We have some directory structures here, convert the paths to underscores for the FishEye name.
    REPONAME=$(echo &quot;$REPOPATH&quot; | sed &#39;&lt;/span&gt;s/&lt;span class=&quot;se&quot;&gt;\/&lt;/span&gt;/_/g&lt;span class=&quot;s1&quot;&gt;&#39;)
    
    # Load the description from the description file in git
    DESCPATH=$(echo &quot;$LINE&quot; | sed &#39;&lt;/span&gt;s/config&lt;span class=&quot;nv&quot;&gt;$/&lt;/span&gt;description/&lt;span class=&quot;s1&quot;&gt;&#39;)
    DESC=$(ssh git@vm-repos cat $DESCPATH)

    # Sidenote, I felt dirty writing this:
    DATA+=&quot;&amp;lt;repository name=\&quot;$REPONAME\&quot; description=\&quot;$DESC\&quot; store-diff=\&quot;true\&quot; enabled=\&quot;true\&quot;&amp;gt;
    &amp;lt;git location=\&quot;git://vm-repos/$REPOPATH\&quot; renames=\&quot;none\&quot;/&amp;gt;
    &amp;lt;security use-defaults=\&quot;true\&quot; allow-anon=\&quot;false\&quot; allow-logged-in=\&quot;false\&quot; allow-anon-write=\&quot;false\&quot; allow-logged-in-write=\&quot;false\&quot;&amp;gt;
        &amp;lt;required-groups/&amp;gt;
        &amp;lt;required-write-groups/&amp;gt;
    &amp;lt;/security&amp;gt;
&amp;lt;/repository&amp;gt;
&quot;
done

# Delete old repos
XMLDATA=$(xmlstarlet ed -d &quot;/config/repository&quot; $XMLFILE)

IFS=$&#39;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&#39;; for line in $XMLDATA; do
    echo $line

    # Insert our new XML after the &amp;lt;/repository-defaults&amp;gt; closing tag.
    if [[ &quot;$line&quot; == *\/repository-defaults* ]]; then
        echo &quot;$DATA&quot;
    fi
done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;This script echoes out a new config.xml. A parent script just shuts down FishEye, executes this and drops the new config.xml in place, and then restarts FishEye.&lt;/p&gt;

&lt;p&gt;Directory syncing, done!&lt;/p&gt;

&lt;h2&gt;Post receive hooks for FishEye updates&lt;/h2&gt;

&lt;p&gt;FishEye was previously polling repos every 15 minutes for changes. This is pretty ugly and inflicts some server load. Thanks to our newly perfectly-aligned FishEye and Git repo names though, writing a post-receive hook to update FishEye via the API became trivial:&lt;/p&gt;

&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;c&quot;&gt;# Replace directory separators with underscores again.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;REPOPATH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt; | sed &lt;span class=&quot;s1&quot;&gt;&#39;s/\/data\/repos\/git\///&#39;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;REPONAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REPOPATH&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | sed &lt;span class=&quot;s1&quot;&gt;&#39;s/\//_/g&#39;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;)&lt;/span&gt;

curl -s -X POST -H &lt;span class=&quot;s2&quot;&gt;&quot;X-Api-Key: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$APIKEY&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; http://&lt;span class=&quot;nv&quot;&gt;$FISHEYE&lt;/span&gt;/rest-service-fecru/admin/repositories-v1/&lt;span class=&quot;nv&quot;&gt;$REPONAME&lt;/span&gt;/scan &amp;gt; /dev/null&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;h2&gt;Harmonious coexistence&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/hipchat-vmrepos-fisheye.png&quot; alt=&quot;FishEye, git, and HipChat working happily together&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now when we do a Git push, we get a message like the one depicted. Project names link to the FishEye project, JIRA ticket numbers link correctly, commit hashes link to the changeset in FishEye, and FishEye&#39;s post-receive hook helps make sure that the commit number links are working in a matter of seconds. Server load is down on both the FishEye and Git servers since we&#39;re not polling every 15 minutes on every repository anymore. And our FishEye coverage is now 100%.&lt;/p&gt;

&lt;p&gt;Huzzahs are in order.&lt;/p&gt;
</content>
	</entry>
	
	
	
	
	
	<entry>
		<title>Disqus Discover</title>
		<link href="http://www.paulzumbrun.com/Disqus-Discover/"/>
		<updated>2013-05-20T00:00:00-05:00</updated>
		<id>http://www.paulzumbrun.com/Disqus-Discover</id>
		<content type="html">&lt;p&gt;The internet&#39;s ubiquitous SaaS commenting widget, Disqus, has a new-ish feature called Discover which they&#39;re pushing heavily for their new sign-ups. Discover is meant to &lt;q&gt;increase your traffic and make money from your website&#39;s engagement&lt;/q&gt;. It&#39;s really a value-added service whereby your comments section links to related featured/paid content on other sites, earning revenue for you. A kind of AdSense for the Disqus network.&lt;/p&gt;

&lt;p&gt;I love it; this is a great &lt;em&gt;idea&lt;/em&gt;. Content producers get wider exposure, site owners earn revenue, readers get linked to relevant related content, and Disqus monetizes.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/Screen%20Shot%202013-05-20%20at%2011.05.11%20AM.png&quot; class=&quot;right&quot; alt=&quot;Screenshot of horrible Disqus content, part 1&quot; /&gt;
Emphasis on great &lt;em&gt;idea&lt;/em&gt;, though. The execution is just terrible. Check out the kind of content I&#39;m getting:&lt;/p&gt;

&lt;p&gt;As far as I can tell, the service is making no attempt to drive users to &lt;em&gt;related&lt;/em&gt; content. And why are these all so seedy? Is there something in my browser history? I checked with a new IP and a clean browser history, nope, same garbage.&lt;/p&gt;

&lt;p&gt;Thankfully this feature is optional. And as of today, it&#39;s turned off. But in the interests of making this a constructive post ...&lt;/p&gt;

&lt;h2&gt;What could Disqus do to make this better?&lt;/h2&gt;

&lt;p&gt;I think a lot of this is fairly obvious, and I&#39;m surprised Disqus considered Discover viable enough to launch in its current state, even as an &lt;abbr title=&quot;minimum viable product for a value added service&quot;&gt;MVP for a VAS&lt;/abbr&gt;.&lt;/p&gt;

&lt;h3&gt;Content Matching&lt;/h3&gt;

&lt;p&gt;Scrape the content of the page, look at HTTP referrers for search keywords. Use this to make sure that Disqus is linking to &lt;em&gt;related&lt;/em&gt; content. Granted, this is not a trivial thing to implement, but there are tools nowadays that make it so it&#39;s not truly hard anymore. To massively over-simplify: Hadoop and a couple Python libraries, and you&#39;re done.&lt;/p&gt;

&lt;p&gt;Once you have Content Matching working, a great way give site owners control of the ads showing on their site would be a &quot;Greed Level&quot; slider. A scale of 1-to-10 of how tightly the site owner want their ads/featured content bound to the current page&#39;s content. Some users want full-on monetization, and they&#39;re okay with linking off to largely unrelated spam posts. Then for someone like me who doesn&#39;t really care about monetization, but thinks that relevant related content would be cool, you could set that to 1 and only show on-topic related posts.&lt;/p&gt;

&lt;h3&gt;Filtering Inappropriate Content&lt;/h3&gt;

&lt;p&gt;A quick survey of my pages shows a majority of the featured content consists of trashy articles about sex, and nude beaches and such. At the &lt;em&gt;bare minimum&lt;/em&gt; they should be providing an option to limit related content to things that could be construed as professional. One of the linked pages was something like the 25 best laptops on the market. Okay, it&#39;s still totally off-topic, but at least it&#39;s not seedy.&lt;/p&gt;

&lt;h3&gt;Content Categorization&lt;/h3&gt;

&lt;p&gt;This is a pretty standard feature for ad networks, to have different topics that the site owner can specifically include or exclude. In lieu of content matching, this would be a good start. This is totally absent from Discover. In fact there are no options whatsoever other than something that seems to only control whether the ad content shows above or below the comments box.&lt;/p&gt;

&lt;h2&gt;Outcome&lt;/h2&gt;

&lt;p&gt;The outcome is that for now I&#39;ve turned this brilliant feature off. I didn&#39;t want to, because I really like the idea, but it&#39;s just too poorly executed to leave on.&lt;/p&gt;

&lt;p&gt;This post will serve as a reminder to me to try this again later, once Disqus has had some time to improve the feature. But for now, Disqus has missed the boat by launching prematurely. I have to imagine a lot of users have tried this, become disenfranchised, and won&#39;t turn it back on.&lt;/p&gt;
</content>
	</entry>
	
	
	
	<entry>
		<title>Setting up S3 and CloudFront for Static Site Hosting</title>
		<link href="http://www.paulzumbrun.com/Setting-up-S3-and-CloudFront-for-static/"/>
		<updated>2013-05-19T00:00:00-05:00</updated>
		<id>http://www.paulzumbrun.com/Setting-up-S3-and-CloudFront-for-static</id>
		<content type="html">&lt;p&gt;&lt;small&gt;(This is a companion post to my post about &lt;a href=&quot;/Introducing-jaws/&quot;&gt;my S3/CloudFront static site deployment script, _jaws&lt;/a&gt;)&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;S3 and CloudFront pair up really nicely to form a static site hosting solution. You can upload your site to S3, avoiding the need for real hosting, and then you can use CloudFront as a CDN to make things fast, all over the world.&lt;/p&gt;

&lt;p&gt;If your blog gets little or no traffic, the total cost will be in the vicinity of $0.10 a month depending on your settings.&lt;/p&gt;

&lt;p&gt;I love this setup for my blog since it means I don&#39;t need to maintain any infrastructure. Sure, I have server assets and VPSes lying around, but worrying about a WordPress blog being hacked is way down on my list of things I want to be doing. And while Amazon&#39;s datacenters have had downtime here and there, just like my Linodes, I have a pretty high level of confidence that things will come back up in a reasonable amount of time and &quot;just work&quot; with this setup. Push it, forget it.&lt;/p&gt;

&lt;p&gt;I&#39;ve also spent a lot of time traveling and living outside of the USA (I&#39;m writing this blog post from my girlfriend&#39;s place in Hong Kong) which has made me particularly sensitive to sites that lack edge servers. It sounds petty, but I really don&#39;t want that extra 300ms of latency if I can avoid it.&lt;/p&gt;

&lt;p&gt;So let&#39;s talk about how to set this up.&lt;/p&gt;

&lt;h2&gt;Step 1: S3&lt;/h2&gt;

&lt;p&gt;S3 setup is super easy. All you have to do is log into AWS and create a bucket.&lt;/p&gt;

&lt;p&gt;There is one trick though. To make things place nice with CloudFront (and not have all your paths have index.html in them), you have to use the &quot;Static Website Hosting&quot; option.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/s3-static-website-hosting.png&quot; alt=&quot;Screenshot of S3 static website hosting setup&quot; /&gt;&lt;/p&gt;

&lt;p&gt;If you don&#39;t do this, you&#39;re going to get confusing &lt;code&gt;AccessDenied&lt;/code&gt; XML error messages from CloudFront down the road.&lt;/p&gt;

&lt;p&gt;Make note of the &lt;strong&gt;Endpoint&lt;/strong&gt; that it shows you.&lt;/p&gt;

&lt;p&gt;You&#39;re now ready to go ahead and upload your files to your S3 bucket.&lt;/p&gt;

&lt;h2&gt;Step 2: CloudFront&lt;/h2&gt;

&lt;p&gt;CloudFront is a bit less obvious. Start off by creating a download distribution. Most of the settings are pretty straightforward, but one very easy mistake to make is when you&#39;re setting the &quot;Origin Domain Name&quot;. Make sure to specify the HTTP URL to your S3 bucket, rather than one of the pre-filled S3 buckets. Otherwise, again, you&#39;ll get those &lt;code&gt;AccessDenied&lt;/code&gt; messages.&lt;/p&gt;

&lt;p&gt;Most settings can be left as the default but I highly recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set your &lt;strong&gt;Default Root Object&lt;/strong&gt; to &lt;code&gt;index.html&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Setting &quot;Forward Query Strings&quot; to &lt;strong&gt;Yes&lt;/strong&gt;. This can be huge for proper caching of linked assets like JavaScript and CSS.&lt;/li&gt;
&lt;li&gt;Setting a couple alternate CNAMEs. One for www.example.com, one for static.example.com, one for images.example.com. The main benefit here is that you can spread your HTTP requests over several different domains, for HTTP efficiency. Not really needed.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;Object Caching&lt;/strong&gt;, I select &lt;strong&gt;Use Origin Cache Headers&lt;/strong&gt;. This is because I know I&#39;m setting these properly via &lt;a href=&quot;/Introducing-jaws/&quot;&gt;_jaws&lt;/a&gt;; if you&#39;re not doing this, you&#39;ll want to Customize and set your own Minimum TTL.&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;After you submit, CloudFront may take a few minutes for things to finish setting up. Once it&#39;s done, add a CNAME record into your DNS such that www.example.com references your CloudFront domain name (mine for example is d1bx34tfj1fr6j.cloudfront.net).&lt;/p&gt;

&lt;p&gt;You should be all set now.&lt;/p&gt;

&lt;p&gt;A couple next steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a deployment script (like my aforementioned &lt;a href=&quot;/Introducing-jaws/&quot;&gt;_jaws&lt;/a&gt;) to upload your site with proper compression and caching.&lt;/li&gt;
&lt;li&gt;Make sure your naked/apex (e.g. example.com) domain 301 redirects to &lt;strong&gt;www.&lt;/strong&gt;example.com. You can&#39;t do a CNAME on your domain apex, so CloudFront won&#39;t work nicely from there. You can use something like &lt;a href=&quot;http://wwwizer.com/naked-domain-redirect&quot;&gt;wwwizer&lt;/a&gt;. I use nginx redirect on Linode I have lying around for some other stuff. S3 can also do this with the &quot;Redirect all requests to another host name&quot; option pictured above.&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;From here, you should be in really good shape. Happy AWSing.&lt;/p&gt;
</content>
	</entry>
	
	
	
	<entry>
		<title>Introducing _jaws</title>
		<link href="http://www.paulzumbrun.com/Introducing-jaws/"/>
		<updated>2013-05-18T00:00:00-05:00</updated>
		<id>http://www.paulzumbrun.com/Introducing-jaws</id>
		<content type="html">&lt;p&gt;&lt;img src=&quot;/img/jaws-large.png&quot; class=&quot;right&quot; alt=&quot;_jaws logo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I&#39;ve been hosting my blog on Amazon&#39;s S3 and CloudFront for a while; I also host a couple other static sites using a similar setup. It&#39;s a wonderful setup once you have it working. Blazing fast, dirt cheap, and the complicated moving parts are all abstracted away.&lt;/p&gt;

&lt;p&gt;The problem was always getting everything up on CloudFront properly. That&#39;s why I wrote &lt;a href=&quot;https://github.com/pauljz/_jaws&quot;&gt;_jaws&lt;/a&gt;: a comprehensive, drop-in, AWS deployment script for Jekyll blogs or other static sites.&lt;/p&gt;

&lt;p&gt;In this post I&#39;ll walk you through the problems that _jaws solves, and how it solves them.&lt;/p&gt;

&lt;h2&gt;Problem: Caching and cache-busting linked assets.&lt;/h2&gt;

&lt;p&gt;You want your truly static assets like CSS, JavaScript, and images to be cached forever. You don&#39;t even want the browser to wait for a 304 Not Modified response. This is easy to do with a &lt;code&gt;Cache-Control: max-age=...&lt;/code&gt; header ... until you have to change your assets. The fix is to cache-bust using the query string -  e.g., &lt;code&gt;/example.js?v=1&lt;/code&gt; - using a version number or a file modification time or something. You can do this per-asset with Jekyll, by having each asset you include have a &lt;code&gt;?ts={{site.time}}&lt;/code&gt; on the end. But this is tedious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; _jaws finds every script include, stylesheet include, and image, and inserts the current git commit on the end. Everything is versioned based on when the code &lt;em&gt;actually changes&lt;/em&gt;, and is permanently cached. We also do a separate &lt;code&gt;s3cmd sync&lt;/code&gt; pass to pass a proper &lt;code&gt;Cache-Control&lt;/code&gt; header for JS/CSS.&lt;/p&gt;

&lt;h2&gt;Problem: Separate hostnames.&lt;/h2&gt;

&lt;p&gt;Admittedly a minor problem. It can be more efficient if CSS, JavaScript and images are on a separate domain. It means these assets can download asynchronously while the HTML continues to load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Simple enough, _jaws does a full pass over HTML assets and inserts a fully-qualified domain name on any included JS, CSS, or image.&lt;/p&gt;

&lt;h2&gt;Problem: Minification.&lt;/h2&gt;

&lt;p&gt;It&#39;s just easier for development to use un-minified JavaScript, CSS, and HTML. But in a production environment, you really want to strip out all that extra weight. Yeah, you could have both minified and un-minified assets and swap between them (for JavaScript), and for CSS you&#39;re probably using something like SASS, LESS or Stylus that will generate minified CSS for you. For HTML you&#39;re almost certainly not minifying (and you probably don&#39;t care). But it&#39;s a good optimization, and it&#39;s nice to have it just work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: _jaws includes YUI Compressor and &lt;a href=&quot;https://code.google.com/p/htmlcompressor/&quot;&gt;HtmlCompressor&lt;/a&gt;, and runs these on every appropriate asset in your project. Nice, tiny assets.&lt;/p&gt;

&lt;h2&gt;Problem: Compression.&lt;/h2&gt;

&lt;p&gt;This is actually where things get tricky and interesting. When you&#39;re serving text assets, you really want those to be gzipped. Neither S3 nor CloudFront do this for you. You &lt;em&gt;can&lt;/em&gt; upload a gzipped file and tell S3 to include a &lt;code&gt;Content-Encoding: gzip&lt;/code&gt; header though.&lt;/p&gt;

&lt;p&gt;As an extra problem, for things like images, audio, or video files, you almost certainly &lt;em&gt;don&#39;t&lt;/em&gt; want them to be gzipped, because they&#39;re already compressed. The additional compression will just slow things down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; The trick is to do several different &lt;code&gt;s3cmd sync&lt;/code&gt; commands. One for gzipped content, one for non-gzipped-content. _jaws finds all the files that make sense to compress, and gzips them either using &lt;code&gt;gzip -9&lt;/code&gt; or with &lt;a href=&quot;https://code.google.com/p/zopfli/&quot;&gt;zopfli&lt;/a&gt; (bonus points for extra compression).&lt;/p&gt;

&lt;h2&gt;Problem: CloudFront Cache Invalidation&lt;/h2&gt;

&lt;p&gt;Sometimes you need your changes to happen &lt;em&gt;now&lt;/em&gt;. Well, bad news. CloudFront won&#39;t let you do that. But 5-10 minutes is attainable using a CloudFront invalidation request.&lt;/p&gt;

&lt;p&gt;Luckily &lt;code&gt;s3cmd&lt;/code&gt; has a &lt;code&gt;--cf-invalidate&lt;/code&gt; command that does this for us. So that was easy.&lt;/p&gt;

&lt;h1&gt;In Conclusion&lt;/h1&gt;

&lt;p&gt;After all this, we now have a properly compressed, minified, and optimized site, served off an enterprise CDN network ... for less than a dollar a month. And now, with _jaws, you can do this with two lines:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git clone git@github.com:pauljz/_jaws.git
cp _jaws/config.sample.sh _jaws/config.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Change a couple configuration settings and you&#39;re done!  For more information and to get started, take a look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.github.com/pauljz/_jaws&quot;&gt;_jaws on GitHub&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;My post on setting up S3 and CloudFront for static sites.&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;&lt;small&gt;I also wanted to throw out a special thank-you to &lt;a href=&quot;https://twitter.com/tobiassjosten&quot;&gt;Tobias Sjösten&lt;/a&gt; whose &lt;a href=&quot;http://vvv.tobiassjosten.net/development/jekyll-blog-on-amazon-s3-and-cloudfront/&quot;&gt;excellent blog post&lt;/a&gt; inspired this whole script.&lt;/small&gt;&lt;/p&gt;
</content>
	</entry>
	
	
	
	<entry>
		<title>Always Google your error messages</title>
		<link href="http://www.paulzumbrun.com/Google-your-error-messages/"/>
		<updated>2013-04-07T00:00:00-05:00</updated>
		<id>http://www.paulzumbrun.com/Google-your-error-messages</id>
		<content type="html">&lt;p&gt;Watching other people debug is fascinating. I occasionally find myself spotting my coworkers or friends as they try to find the cause of a bug or the fix for a crash. Next time you get called over to someone&#39;s desk, &lt;em&gt;really watch&lt;/em&gt; as they show you the problem, and show you what they&#39;ve tried. Of course, you&#39;re helping to look for the fix, but also pay attention to them. There&#39;s a big opportunity here if you study &lt;em&gt;their&lt;/em&gt; approach as well.&lt;/p&gt;

&lt;p&gt;You can learn a lot about how a person thinks about and internalizes code and architecture by watching the way they try to fix a bug. Methodologies and thought-processes are more different than you would ever expect. I respect a healthy difference in debugging approaches &amp;mdash; it means a friend is more likely to find a fix for something that I overlooked, and vice-versa &amp;mdash; but there&#39;s one thing that always bothers me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When people don&#39;t Google their error messages.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The number of times I&#39;ve seen someone get a cryptic error message, say &lt;q&gt;What the hell does that mean?!&lt;/q&gt;, and then go back to rummaging in their code ... disturbing.&lt;/p&gt;

&lt;p&gt;Unless it&#39;s an error from your in-house code, or you&#39;re working on something &lt;em&gt;incredibly&lt;/em&gt; niche, chances are that someone has had the same problem as you in the past. Any time it looks like your error might be due to anything other than your own software, plug it into Google! It works!&lt;/p&gt;

&lt;p&gt;Thanks in large part to StackOverflow and the other StackExchange sites, but also to a proliferation of domain-specific forums, you&#39;ll almost always get results. And you&#39;ll often get an answer too.&lt;/p&gt;

&lt;p&gt;Maybe a quarter of the time you&#39;ll find threads where someone has had the problem, and a few people have discussed it, and no fix has shown up. Even then it&#39;s not a wash, since you just invested all of 2 minutes to learn a handful of things that &lt;em&gt;don&#39;t&lt;/em&gt; work.&lt;/p&gt;

&lt;p&gt;Of course, every now and then you get a &lt;a href=&quot;http://xkcd.com/979/&quot;&gt;soul-crushing defeat&lt;/a&gt;, but in the past few years this has become even rarer for me.&lt;/p&gt;

&lt;h2&gt;Let&#39;s do a case study, just for fun.&lt;/h2&gt;

&lt;p&gt;This one was just today. I was debugging an error in a new client&#39;s mod_perl/Embperl application. All the client has is their production server &amp;mdash; their original dev environment was eaten by a former contractor &amp;mdash; so the first thing we did was build out a proper dev environment again. If you&#39;ve ever installed someone else&#39;s Perl application, you&#39;re probably familiar with the hell of missing, undocumented CPAN module dependencies. This was no exception. About 40 CPAN modules later we had a working environment (had to Google a few errors there for unresolved dependencies). Except we found out a few AJAX requests would crash with this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/usr/sbin/apache2: symbol lookup error: /usr/lib/perl5/auto/APR/Request/Apache2/Apache2.so: undefined symbol: apreq_handle_apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Huh? Version incompatibility between Perl&#39;s Apache2.so and our Apache or mod_perl version? Bug in the CPAN module? Library misuse? A Google search later yielded &lt;a href=&quot;http://foswiki.org/Tasks/Item1819&quot;&gt;this thread&lt;/a&gt;, where I found out I just had to run &lt;code&gt;sudo a2enmod apreq&lt;/code&gt;. I didn&#39;t even know there was an apreq module for Apache. But I now had my fix in about 30 seconds of seeing the error.&lt;/p&gt;

&lt;p&gt;I&#39;ll try to keep appending more case studies as they come up, but feel free to add your own in the comments!&lt;/p&gt;
</content>
	</entry>
	
	
	
	<entry>
		<title>Embedding escaped code in a Jade template</title>
		<link href="http://www.paulzumbrun.com/posts/jade-and-google-code-prettify/"/>
		<updated>2012-08-06T00:00:00-05:00</updated>
		<id>http://www.paulzumbrun.com/posts/Embedding-escaped-code-in-a-Jade-template</id>
		<content type="html">&lt;p&gt;A while back I had built a bit of a blogging platform using the ubiquitous &lt;a href=&quot;http://nodejs.org/&quot;&gt;node.js&lt;/a&gt; stack of &lt;a href=&quot;http://expressjs.com/&quot;&gt;Express&lt;/a&gt; and &lt;a href=&quot;http://jade-lang.com/&quot;&gt;Jade&lt;/a&gt; templates. It was pretty straight-forward &amp;mdash; blog posts were just written in Jade and served with Express &amp;mdash; but syntax highlighting ended up being a problem.&lt;/p&gt;

&lt;p&gt;I had opted to use &lt;a href=&quot;http://code.google.com/p/google-code-prettify/&quot;&gt;google-code-prettify&lt;/a&gt;, which takes an &lt;em&gt;escaped&lt;/em&gt; &amp;lt;code&amp;gt; block of HTML like this and highlights it:&lt;/p&gt;

&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;pre&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;prettyprint&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;code&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;ni&quot;&gt;&amp;amp;lt;&lt;/span&gt;html&lt;span class=&quot;ni&quot;&gt;&amp;amp;gt;&lt;/span&gt;
            &lt;span class=&quot;ni&quot;&gt;&amp;amp;lt;&lt;/span&gt;head&lt;span class=&quot;ni&quot;&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;/head&lt;span class=&quot;ni&quot;&gt;&amp;amp;gt;&lt;/span&gt;
            &lt;span class=&quot;ni&quot;&gt;&amp;amp;lt;&lt;/span&gt;body&lt;span class=&quot;ni&quot;&gt;&amp;amp;gt;&amp;amp;lt;&lt;/span&gt;/body&lt;span class=&quot;ni&quot;&gt;&amp;amp;gt;&lt;/span&gt;
        &lt;span class=&quot;ni&quot;&gt;&amp;amp;lt;&lt;/span&gt;/html&lt;span class=&quot;ni&quot;&gt;&amp;amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;I really didn&#39;t want to pre-escape my code, of course. That&#39;s horrible. And as far as I can tell, Jade has no way to automatically escape HTML like this, unless it&#39;s already stored in a variable.&lt;/p&gt;

&lt;h2&gt;Solution: Custom Filters&lt;/h2&gt;

&lt;p&gt;Jade&#39;s documentation briefly mentions &lt;a href=&quot;https://github.com/visionmedia/jade#a7&quot;&gt;filters&lt;/a&gt;. The syntax is just a word prefixed with a colon.  There are a few canned ones like :markdown.  I jumped in and wrote my own custom filter, :code, to strip out significant HTML characters and replace them with &amp;amp;lt; &amp;amp;gt; and the like.&lt;/p&gt;

&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&#39;jade&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;block&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;block&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&amp;amp;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;&amp;amp;amp;&#39;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;&amp;amp;lt;&#39;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&amp;gt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;&amp;amp;gt;&#39;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&quot;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;&amp;amp;quot;&#39;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;Cryptic errors ensued.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SyntaxError: Unexpected token ILLEGAL
    at Object.Function (unknown source)
    at Object.compile (/Users/paul/Code/paulzumbrun.com/node_modules/jade/lib/jade.js:176:8)
    at Function.compile (/Users/paul/Code/paulzumbrun.com/node_modules/express/lib/view.js:68:33)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Turns out the result still gets parsed by Jade; it&#39;s not just a string that gets dropped in like I thought. This is actually a good thing because it means you can chain filters, but we have to work around it. To make the parser happy with me again I had to strip out a few extra characters. Newlines needed to become \n. Other escape sequences needed double-escaped. Hashes need stripped too, since they&#39;re used to execute JavaScript inline &amp;#35;{thusly}.&lt;/p&gt;

&lt;p&gt;This is the final working filter.&lt;/p&gt;

&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&#39;jade&#39;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;block&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;block&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&amp;amp;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;&amp;amp;amp;&#39;&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;&amp;amp;lt;&#39;&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&amp;gt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;&amp;amp;gt;&#39;&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&quot;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;&amp;amp;quot;&#39;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/#/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;&amp;amp;#35;&#39;&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;\\\\&#39;&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&#39;\\n&#39;&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;Now when you go to use this in a template, all you have to do is this. Pretty close to my original goal.&lt;/p&gt;

&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-jade&quot; data-lang=&quot;jade&quot;&gt;pre.prettyprint
    code.lang-html
        :code
            &amp;lt;html&amp;gt;
                &amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;
                &amp;lt;body&amp;gt;&amp;lt;/body&amp;gt;
            &amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;There may be a cleaner way to do this, but I sure couldn&#39;t find it with the search terms I was using.&lt;/p&gt;
</content>
	</entry>
	
	
</feed>
