<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Webhook Relay</title>
    <description>The latest articles on Forem by Webhook Relay (@webhookrelay).</description>
    <link>https://forem.com/webhookrelay</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1179%2Fd2551bd8-3962-4e49-9ec3-f4abe1d09d4c.png</url>
      <title>Forem: Webhook Relay</title>
      <link>https://forem.com/webhookrelay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/webhookrelay"/>
    <language>en</language>
    <item>
      <title>Building simple Airtable integrations 🌟🔥</title>
      <dc:creator>war-bunny</dc:creator>
      <pubDate>Mon, 02 Oct 2023 10:41:57 +0000</pubDate>
      <link>https://forem.com/webhookrelay/building-simple-airtable-integrations-1m9j</link>
      <guid>https://forem.com/webhookrelay/building-simple-airtable-integrations-1m9j</guid>
      <description>&lt;p&gt;Let's learn how to integrate Airtable. In the first example, we will receive an HTML form from a website and add a row to Airtable. This can be used for:&lt;/p&gt;

&lt;p&gt;You can use this form to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allow users to contact you&lt;/li&gt;
&lt;li&gt;A way for users to submit bug reports&lt;/li&gt;
&lt;li&gt;Collect user feedback&lt;/li&gt;
&lt;li&gt;Waitlist signups for your product&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A5CKSMQ9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nlix1j4gy15onhp64fk5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A5CKSMQ9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nlix1j4gy15onhp64fk5.png" alt="contact-form-synpse" width="800" height="660"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Example form on &lt;a href="https://synpse.net/contact/"&gt;https://synpse.net/contact/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://my.webhookrelay.com"&gt;Webhook Relay account&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://airtable.com//"&gt;Airtable&lt;/a&gt; account with a workspace.&lt;/li&gt;
&lt;li&gt;Static website of your choice. We are using websites hosted both on GitHub and Cloudflare pages, however with this setup it doesn't matter where the website is hosted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prepare Airtable
&lt;/h2&gt;

&lt;p&gt;In order to start adding rows to Airtable you will need to prepare few things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new base&lt;/li&gt;
&lt;li&gt;Find your base ID and table name. To do this, check your URL when you are in the table view. It should look something like this: &lt;code&gt;https://airtable.com/appXXXX/tblXXXX/viwtXXXX&lt;/code&gt;. The first part is your base ID and the second part is your table name. From here, &lt;code&gt;appXXXX&lt;/code&gt; is the base ID and &lt;code&gt;tblXXXX&lt;/code&gt; is the table ID.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create forwarding configuration
&lt;/h2&gt;

&lt;p&gt;You will need to create forwarding config to a public destination here &lt;a href="https://my.webhookrelay.com/new-public-destination"&gt;https://my.webhookrelay.com/new-public-destination&lt;/a&gt;. The URL will be &lt;code&gt;https://api.airtable.com/v0/appXXXX/tblXXXX&lt;br&gt;
&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚨 Don't just copy/paste the URL from the browser! they are not the same as your browser uses &lt;code&gt;https://airtable.com/appXXXX/tblXXXX/viwtXXXX&lt;/code&gt; but we need to send webhooks to &lt;code&gt;https://api.airtable.com/v0/appXXXX/tblXXXX&lt;/code&gt; 🚨&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aKD9ekfM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jlptiyn2c59x32vfj7wp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aKD9ekfM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jlptiyn2c59x32vfj7wp.png" alt="create-forwarding-configuration" width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Don't try to send requests to this yet as we will need to set up an Airtable webhook integration first.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setup Airtable webhook integration
&lt;/h2&gt;

&lt;p&gt;Next step is to transform the incoming HTML form request into a webhook that will add or create a new record in your Airtable table:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the &lt;a href="https://my.webhookrelay.com/functions"&gt;Functions&lt;/a&gt; page&lt;/li&gt;
&lt;li&gt;Create a new function and copy paste the code into the composer:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestMethod&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; 
    &lt;span class="c1"&gt;-- Only POST requests allowed &lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;StopForwarding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;-- Taking fields from the HTML form and &lt;/span&gt;
&lt;span class="c1"&gt;-- using them to prepare Airtable webhook&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;encoded_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestFormData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestFormData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="n"&gt;Added&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unix&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s2"&gt;"2006-01-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"UTC"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestFormData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;-- Encoding payload to JSON&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;encoded_payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoded_payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="nb"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;SetRequestHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Bearer "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;GetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SECRET_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;SetRequestHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;SetRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoded_payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;SetRequestMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once added:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get your personal access token from here &lt;a href="https://airtable.com/create/tokens"&gt;https://airtable.com/create/tokens&lt;/a&gt;. Give it a name and ensure it has &lt;code&gt;data.records:write&lt;/code&gt; &lt;strong&gt;scope&lt;/strong&gt; and set &lt;strong&gt;Access&lt;/strong&gt; to the base you want to add records to.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;CONFIG VARIABLES&lt;/code&gt; tab of the function and create a new variable &lt;code&gt;SECRET_API_TOKEN&lt;/code&gt; with the value of your personal access token:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mF9NuxFj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q03qsya9tw1qe6swlqdw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mF9NuxFj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q03qsya9tw1qe6swlqdw.png" alt="configuration-variables" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting up HTML form
&lt;/h2&gt;

&lt;p&gt;We will use a simple HTML script. Replace the &lt;code&gt;https://XXXXXX.hooks.webhookrelay.com&lt;/code&gt; URL with your own Webhook Relay input endpoint. You can find it in the Bucket details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"form-i"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Forwarding to Webhook Relay which transforms requests and adds it to the Airtable as a new row --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"https://XXXXXX.hooks.webhookrelay.com"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"form-i"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Name:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Message: &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;rows=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;cols=&lt;/span&gt;&lt;span class="s"&gt;"40"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have made a simple CodePen example here &lt;a href="https://codepen.io/defiant77/pen/gOQeRMm"&gt;https://codepen.io/defiant77/pen/gOQeRMm&lt;/a&gt; which you can open and edit the webhooks URL with the one from your bucket.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YU5ZHVud--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cdqx0srhkhg5n02shi94.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YU5ZHVud--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cdqx0srhkhg5n02shi94.png" alt="html-form-to-airtable" width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it out
&lt;/h2&gt;

&lt;p&gt;Fill in the form and click submit. In your Airtable you should be able to see the new record. If you can't, check out webhook logs in Webhook Relay's bucket details. &lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;There are very few moving parts here but if something doesn't work, things to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Form &lt;code&gt;action&lt;/code&gt; URL is pointing at the bucket that you have created.&lt;/li&gt;
&lt;li&gt;Function is attached to the output. It's required to have it as it needs to do the transformation.&lt;/li&gt;
&lt;li&gt;The destination URL should be pointing at the correct base ID and table ID in Airtable.&lt;/li&gt;
&lt;li&gt;Personal access token needs to have permissions to write (&lt;code&gt;data.records:write&lt;/code&gt;) and access is allowed to the base that you are working with.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it! 😃 You can also make a very similar integration for Google Sheets as the process is the same - using webhook as an API call.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally posted on Webhook Relay blog: &lt;br&gt;
&lt;a href="https://webhookrelay.com/blog/2023/08/07/airtable-integrations/"&gt;https://webhookrelay.com/blog/2023/08/07/airtable-integrations/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>airtable</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Easy and secure Jenkins Operator deployment on Kubernetes</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Mon, 06 Jul 2020 10:23:45 +0000</pubDate>
      <link>https://forem.com/webhookrelay/easy-and-secure-jenkins-operator-deployment-on-kubernetes-1jn8</link>
      <guid>https://forem.com/webhookrelay/easy-and-secure-jenkins-operator-deployment-on-kubernetes-1jn8</guid>
      <description>&lt;p&gt;In this tutorial, we will configure a Jenkins pipeline on Kubernetes that leverages Jenkins and Webhook Relay operators. Jenkins Kubernetes operator will be creating Jenkins instances with a predefined seed job. Webhook Relay operator will ensure that GitHub webhooks on push events trigger new Jenkins builds for a fast and efficient CI/CD experience.&lt;/p&gt;

&lt;p&gt;Advantages of this setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your Jenkins instance is only accessible through kubectl port-forward while maintaining the ability to receive webhooks from public destinations.&lt;/li&gt;
&lt;li&gt;Jenkins pipeline configuration is stored in Git.&lt;/li&gt;
&lt;li&gt;Webhook Relay routing configuration is stored in Git, the same as the Jenkins itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can read about operator pattern in &lt;a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/"&gt;Kubernetes docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Prerequisites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.helm.sh/using_helm/#installing-helm"&gt;Helm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://my.webhookrelay.com"&gt;Webhook Relay account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/"&gt;Kubernetes&lt;/a&gt; environment, Minikube, k3s, GKE, AKS, etc. are fine.&lt;/li&gt;
&lt;li&gt;Configured kubectl&lt;/li&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;The installation will consist of several steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installing Jenkins operator&lt;/li&gt;
&lt;li&gt;Installing Webhook Relay operator&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create a fresh namespace
&lt;/h2&gt;

&lt;p&gt;Let's start by creating a new namespace where we will put our Jenkins instance and run builds. I will call it 'jenkins' but you can choose any other name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace jenkins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then switch to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl config set-context &lt;span class="si"&gt;$(&lt;/span&gt;kubectl config current-context&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;jenkins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Jenkins Operator
&lt;/h2&gt;

&lt;p&gt;We will install Jenkins operator using Helm. First, add the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add jenkins https://raw.githubusercontent.com/jenkinsci/kubernetes-operator/master/chart
helm repo update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the repository has been added, install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;jenkins-operator jenkins/jenkins-operator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Official docs can be found here: &lt;a href="https://jenkinsci.github.io/kubernetes-operator/docs/installation/"&gt;https://jenkinsci.github.io/kubernetes-operator/docs/installation/&lt;/a&gt;. The operator is not the Jenkins itself so to get our Jenkins instance, we will have to create a &lt;a href="https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/"&gt;Custom Resource&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Custom resources are extensions of the Kubernetes API. This page discusses when to add a custom resource to your Kubernetes cluster and when to use a standalone service. It describes the two methods for adding custom resources and how to choose between them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Start Jenkins (using Custom Resource)
&lt;/h2&gt;

&lt;p&gt;We will need to create a CR. You can either use Jenkins Operator docs to create one or you can fork this &lt;a href="https://github.com/webhookrelay/jenkins-operator-example.git"&gt;https://github.com/webhookrelay/jenkins-operator-example.git&lt;/a&gt; repository and clone it. Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update &lt;strong&gt;jenkins_cr.yaml&lt;/strong&gt; &lt;a href="https://github.com/webhookrelay/jenkins-operator-example/blob/master/jenkins_cr.yaml"&gt;file&lt;/a&gt; &lt;code&gt;https://github.com/webhookrelay/jenkins-operator-example.git&lt;/code&gt; to your own repository fork (it will usually be &lt;code&gt;https://github.com/&amp;lt;your username or organization&amp;gt;/jenkins-operator-example.git&lt;/code&gt;). &lt;/li&gt;
&lt;li&gt;Create it with kubectl:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; jenkins_cr.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Main differences in this file from the stock example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added &lt;code&gt;github&lt;/code&gt; plugin as we will need it to trigger jobs&lt;/li&gt;
&lt;li&gt;Seed job got &lt;code&gt;githubPushTrigger: true&lt;/code&gt; set as well&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Creating this PR should result in two additional containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods
NAME                                      READY   STATUS    RESTARTS   AGE
jenkins-jenkins                           1/1     Running   0          7m11s
jenkins-operator-6dbbc458c9-gmx6p         1/1     Running   0          18m
seed-job-agent-jenkins-65cc4bc684-9ztr5   1/1     Running   0          6m21s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's connect to Jenkins. First, get username and password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;--namespace&lt;/span&gt; jenkins get secret jenkins-operator-credentials-jenkins &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'jsonpath={.data.user}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
kubectl &lt;span class="nt"&gt;--namespace&lt;/span&gt; jenkins get secret jenkins-operator-credentials-jenkins &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'jsonpath={.data.password}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in one terminal start port forwarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward jenkins-jenkins 8080:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then just open &lt;a href="http://localhost:8080"&gt;http://localhost:8080&lt;/a&gt; in your browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eD9s8cT2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/qonhowe4fy0r5ndml1ee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eD9s8cT2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/qonhowe4fy0r5ndml1ee.png" alt="Jenkins dashboard" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhook Relay
&lt;/h2&gt;

&lt;p&gt;Retrieve your access token key &amp;amp; secret pair from &lt;a href="https://my.webhookrelay.com/tokens"&gt;https://my.webhookrelay.com/tokens&lt;/a&gt; and set them as an environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RELAY_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxxxxxxxxxxx
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RELAY_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add Webhook Relay Operator Helm repository and install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add webhookrelay https://charts.webhookrelay.com
helm repo update
helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; webhookrelay-operator &lt;span class="nt"&gt;--namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;jenkins webhookrelay/webhookrelay-operator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; credentials.key&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$RELAY_KEY&lt;/span&gt; &lt;span class="nt"&gt;--set&lt;/span&gt; credentials.secret&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$RELAY_SECRET&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Operator doesn't forward webhooks on its own. Each created CR will ensure an agent deployment that is configured to route specific buckets.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From the &lt;a href="https://github.com/webhookrelay/jenkins-operator-example/blob/master/webhookrelay_cr.yaml"&gt;operator example repository&lt;/a&gt; we will need to create Webhook Relay Custom Resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; webhookrelay_cr.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note that if you have modified Jenkins CR name you will need to update webhookrelay_cr.yaml "destination" field from &lt;code&gt;destination: http://jenkins-operator-http-jenkins:8080/github-webhook/&lt;/code&gt; to whatever your current Jenkins service is. Typically it will be in a format &lt;code&gt;jenkins-operator-http-&amp;lt;CR name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  GitHub Configuration
&lt;/h2&gt;

&lt;p&gt;This step could be automated by making Jenkins automatically configure Github repositories to forwarding to this endpoint, however for simplicity and so that it's more clear how it works, we will add this URL manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get your Webhook Relay public URL
&lt;/h3&gt;

&lt;p&gt;To get your public endpoint you can either visit &lt;a href="https://my.webhookrelay.com/buckets"&gt;https://my.webhookrelay.com/buckets&lt;/a&gt; page or get it via CR status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get webhookrelayforwards.forward.webhookrelay.com forward-to-jenkins &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'jsonpath={.status.publicEndpoints[0]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result should look something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get webhookrelayforwards.forward.webhookrelay.com forward-to-jenkins &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'jsonpath={.status.publicEndpoints[0]}'&lt;/span&gt;
https://k0yv9ip5sxxp55ncsu936k.hooks.webhookrelay.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add public URL to GitHub repository settings
&lt;/h3&gt;

&lt;p&gt;Take the public endpoint URL and add it to your GitHub repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ax9ikjbT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/g16gtp9hjdjk6k6ouir0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ax9ikjbT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/g16gtp9hjdjk6k6ouir0.png" alt="GitHub configuration" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the pipeline
&lt;/h2&gt;

&lt;p&gt;First, when the pipeline is created, trigger the build manually. After that, any push to your GitHub repository will send a webhook through Webhook Relay to your Jenkins instance that's running inside a Kubernetes cluster:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--O8Fz5ZZM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/x1fw832ss58naya1nc1j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--O8Fz5ZZM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/x1fw832ss58naya1nc1j.png" alt="Jenkins pipeline" width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This tutorial was initially published on Webhook Relay website: &lt;a href="https://webhookrelay.com/v1/tutorials/webhooks-jenkins-operator-kubernetes.html"&gt;https://webhookrelay.com/v1/tutorials/webhooks-jenkins-operator-kubernetes.html&lt;/a&gt;&lt;/strong&gt; &lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Serverless webhook transformation (DockerHub to Slack)</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Fri, 28 Feb 2020 08:49:56 +0000</pubDate>
      <link>https://forem.com/webhookrelay/serverless-webhook-transformation-dockerhub-to-slack-4j9d</link>
      <guid>https://forem.com/webhookrelay/serverless-webhook-transformation-dockerhub-to-slack-4j9d</guid>
      <description>&lt;p&gt;Many Docker registries provide a way to notify team in chat channels when new images are pushed (if you are waiting for a build complete). Let's add this capability to the official DockerHub registry! :) &lt;/p&gt;

&lt;p&gt;We will use newly released &lt;a href="https://webhookrelay.com/v1/guide/functions.html"&gt;Webhook Relay Functions&lt;/a&gt; to transform webhooks while they are passing through the service.&lt;/p&gt;

&lt;p&gt;Prerequisites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://my.webhookrelay.com/"&gt;Webhook Relay account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hub.docker.com/"&gt;DockerHub account&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create a bucket and configure DockerHub notification
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a bucket here &lt;a href="https://my.webhookrelay.com/buckets"&gt;https://my.webhookrelay.com/buckets&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Once you have it, in the inputs section you will find your public input endpoint, copy it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Bek8hc5u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/hb0nbhnm0foyazklue94.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Bek8hc5u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/hb0nbhnm0foyazklue94.png" alt="Input endpoint URL" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add a new DockerHub webhook setting pointing at our public input endpoint (&lt;a href="https://docs.docker.com/docker-hub/webhooks/"&gt;DockerHub docs&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7USp9W5X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/4v3w4vhr6tt757a8gppd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7USp9W5X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/4v3w4vhr6tt757a8gppd.png" alt="DockerHub config" width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Get a sample of DockerHub webhook
&lt;/h3&gt;

&lt;p&gt;Push a new Docker image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker push karolisr/demo-webhook:latest
The push refers to repository [docker.io/karolisr/demo-webhook]
48bd38e03c42: Mounted from karolisr/webhook-demo
fd9f9fbd5947: Mounted from karolisr/webhook-demo
5216338b40a7: Mounted from karolisr/webhook-demo
latest: digest: sha256:703f2bab2ce8df0c5ec4e45e26718954b09bf4a625ab831c6556fd27d60f1325 size: 949
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We should be able to see a new incoming webhook. It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"push_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"pushed_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1582839308&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"images"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"pusher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"karolisr"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"callback_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://registry.hub.docker.com/u/karolisr/demo-webhook/hook/242ii4ddc2jji4a0cc44fbcdbcdecj1ab/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"is_trusted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"full_description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"repo_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://hub.docker.com/r/karolisr/demo-webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"karolisr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"is_official"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"is_private"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo-webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"karolisr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"star_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"comment_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"date_created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1524557040&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"repo_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"karolisr/demo-webhook"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a Function to transform the webhook
&lt;/h3&gt;

&lt;p&gt;Go to the &lt;a href="https://my.webhookrelay.com/functions"&gt;Functions page&lt;/a&gt; and click on a "Create Function" button. Enter a name, for example "dockerhub-to-slack" and click "Submit".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For this example we are using &lt;a href="https://www.lua.org/start.html"&gt;Lua language&lt;/a&gt; which provides an easy way to parse, transform payloads. Webhook Relay Functions also support WebAssembly modules, however they are currently under development and can only be added/updated using relay CLI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can now copy/paste webhook payload into the "request body" area for later tests. In the code editor let's add a Lua function to get repository name and prepare a Slack webhook payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="nb"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"New image pushed at: "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;"repo_name"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;":"&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"push_data"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;"tag"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;-- Preparing Slack payload&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;response_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"in_channel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- Set request header to application/json&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;SetRequestHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Set request method to PUT&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;SetRequestMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Set modified request body&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;SetRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click "Save" and then try testing it with the "Send" button:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PJjqKf0Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/icxxw26h9fhifeqnqfxy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PJjqKf0Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/icxxw26h9fhifeqnqfxy.png" alt="Function invoke example" width="800" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect everything together
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Navigate to &lt;a href="https://api.slack.com/messaging/webhooks"&gt;https://api.slack.com/messaging/webhooks&lt;/a&gt; and click "Create your Slack app". Select your workspace, enter a name that you will remember.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a new incoming webhook configuration, copy "Webhook URL" (it starts with &lt;code&gt;https://hooks.slack.com/services/T3...&lt;/code&gt;), we will need to supply it to Webhook Relay.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open your bucket details (via &lt;a href="https://my.webhookrelay.com/buckets"&gt;https://my.webhookrelay.com/buckets&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open "OUTPUT DESTINATIONS" tab and create a new output called "Slack" with the Slack URL from step 2:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LtkSkR_8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/byd869wvndrrj1po5ioo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LtkSkR_8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/byd869wvndrrj1po5ioo.png" alt="Create destination" width="800" height="303"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Once created, click on the "code" symbol and from the dropdown select &lt;code&gt;dockerhub_to_slack&lt;/code&gt; function:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--g6-kMyCo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/4zl0bug70v022dv01xjd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--g6-kMyCo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/4zl0bug70v022dv01xjd.png" alt="Select function" width="800" height="199"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Push new image to DockerHub, you should see a new notification in your Slack channel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NADNc-tN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/prkk6jdldn4srmsmanbl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NADNc-tN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/prkk6jdldn4srmsmanbl.png" alt="Slack notification" width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it, feel free to continue modifying Lua function to include pusher's name and message format. Following this process you can transform any webhook into any other webhook.&lt;/p&gt;

&lt;p&gt;Have fun!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally released on &lt;a href="https://webhookrelay.com/v1/examples/convert-dockerhub-webhook-to-slack.html"&gt;https://webhookrelay.com/v1/examples/convert-dockerhub-webhook-to-slack.html&lt;/a&gt;&lt;/em&gt; &lt;/p&gt;

</description>
      <category>serverless</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>docker</category>
    </item>
    <item>
      <title>A simple way to keep your Vue page title in sync with the router</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Sun, 08 Sep 2019 22:11:10 +0000</pubDate>
      <link>https://forem.com/webhookrelay/a-simple-way-to-keep-your-vue-page-title-in-sync-with-the-router-ec0</link>
      <guid>https://forem.com/webhookrelay/a-simple-way-to-keep-your-vue-page-title-in-sync-with-the-router-ec0</guid>
      <description>&lt;p&gt;I have noticed in quite a few projects that developers forget to keep page title updated with the router or maybe think that they will do it tomorrow as it is such a small feature :). It always makes sense to keep the title synchronized with the contents for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;helps users with more than one tab &lt;/li&gt;
&lt;li&gt;important for website analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will show you how to do it with the standard &lt;a href="https://github.com/vuejs/vue-router"&gt;vue-router&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Declare route meta in your router config
&lt;/h2&gt;

&lt;p&gt;First things first, let's add some additional metadata to our standard routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LoginPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/buckets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;buckets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BucketsPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Buckets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More docs on the router package can be found &lt;a href="https://router.vuejs.org/guide/essentials/dynamic-matching.html#reacting-to-params-changes"&gt;in the official vue router website&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add $route watcher in your App.vue
&lt;/h2&gt;

&lt;p&gt;Go to your Vue main .vue file (it should have a &lt;code&gt;&amp;lt;router-view&amp;gt;&amp;lt;/router-view&amp;gt;&lt;/code&gt; component) and add a watcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$route&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Website&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will set a title as your page title (if you specified a meta field for that route). I have tried to achieve the same with router navigation guards but &lt;code&gt;$route&lt;/code&gt; watcher seemed like the simplest solution. &lt;/p&gt;

&lt;p&gt;Looks easy, right? :) Feel free to experiment and define more fields in the router meta like page description and anything else you want to be set for that page. Then modify the watcher to also use them. &lt;/p&gt;

&lt;p&gt;Hope this helps!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>vue</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Creating a simple Vue pluralize filter</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Sat, 07 Sep 2019 17:50:14 +0000</pubDate>
      <link>https://forem.com/webhookrelay/creating-a-simple-vue-pluralize-filter-j9m</link>
      <guid>https://forem.com/webhookrelay/creating-a-simple-vue-pluralize-filter-j9m</guid>
      <description>&lt;p&gt;There are many options how to create a pluralize function but in Vue you should probably use filters. Let's create one as it's always handy to have one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Add pluralize package
&lt;/h2&gt;

&lt;p&gt;Let's use &lt;a href="https://github.com/blakeembrey/pluralize"&gt;https://github.com/blakeembrey/pluralize&lt;/a&gt; to do the heavy lifting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add pluralize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Register filter
&lt;/h2&gt;

&lt;p&gt;Our pluralize filter will take two arguments - first one is the left variable in the filter and second is the filter function argument (depends on your code structure, but it's usually &lt;em&gt;main.js&lt;/em&gt; or a dedicated filters file):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .. your other imports&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pluralize&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pluralize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// .. your other code&lt;/span&gt;

&lt;span class="nx"&gt;Vue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pluralize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pluralize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Use the filter!
&lt;/h2&gt;

&lt;p&gt;Now, to use the filter on the left we give it the word we want to pluralize and as an argument we pass the count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{{ tunnels }} {{ 'tunnel' | pluralize(tunnels) }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it :) Now, based on &lt;strong&gt;tunnels&lt;/strong&gt; variable it will be either '1 tunnel' or '50 tunnels'.&lt;/p&gt;

&lt;p&gt;I hope this will be useful to you once you need it! &lt;/p&gt;

</description>
      <category>vue</category>
      <category>beginners</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Docker Compose update on GitHub release webhook</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Mon, 02 Sep 2019 17:46:52 +0000</pubDate>
      <link>https://forem.com/webhookrelay/docker-compose-update-on-github-release-webhook-fch</link>
      <guid>https://forem.com/webhookrelay/docker-compose-update-on-github-release-webhook-fch</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--g2p7rO6_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/6w0sncnp3fck4ppuvhtj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--g2p7rO6_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/6w0sncnp3fck4ppuvhtj.png" alt="Alt Text" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last year I wrote a blog post about &lt;a href="https://dev.to/blog/2018/07/17/auto-deploy-on-git-push"&gt;combining several tools&lt;/a&gt; to automate simple NodeJS app updates on git push. Many users were solving similar problems by writing local web servers in Ruby, Python or PHP to receive webhooks and then do async processing. I am happy to announce that we finally decided to add this feature to the &lt;a href="https://dev.to/v1/installation/cli"&gt;relay CLI&lt;/a&gt;. Now, to execute a bash script, you can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;relay forward &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-bucket-name &lt;span class="nt"&gt;--relayer&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--command&lt;/span&gt; bash update.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to launch a Python app on webhook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;relay forward &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-bucket-name &lt;span class="nt"&gt;--relayer&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--command&lt;/span&gt; python my-app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens up some interesting posibilities to create pipelines that can react to pretty much anything that emits webhooks. In this article I will show you how to build a GitOps style pipeline that keeps a Docker Compose deployment in sync with a docker-compose.yaml hosted on a git repository. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker &amp;amp; &lt;a href="https://docs.docker.com/compose/install/"&gt;Docker Compose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://my.webhookrelay.com/login"&gt;Webhook Relay account&lt;/a&gt; with configured &lt;a href="https://webhookrelay.com/v1/installation/cli"&gt;relay CLI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com"&gt;Github account&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repository with scripts that I used for this article can be found here: &lt;a href="https://github.com/webhookrelay/docker-compose-update-on-git-push"&gt;https://github.com/webhookrelay/docker-compose-update-on-git-push&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Deploying containers through Docker Compose
&lt;/h2&gt;

&lt;p&gt;First step is to do the initial deployment. We will create a simple dockerized Python application that you can find &lt;a href="https://github.com/webhookrelay/docker-compose-update-on-git-push"&gt;here&lt;/a&gt; that connects to Redis and deploy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;karolisr/python-counter:0.1.0"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5000:5000"&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis:alpine"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Setting up updates on Github tag
&lt;/h2&gt;

&lt;p&gt;Since we only want to update on git tags and not just any pushes, let's configure a webhook and analyse the payload.&lt;/p&gt;

&lt;p&gt;To achieve that, let's first create a bucket with an internal output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;relay forward &lt;span class="nt"&gt;--bucket&lt;/span&gt; docker-compose-update-on-git-push http://localhost:4000
Forwarding:
https://my.webhookrelay.com/v1/webhooks/a956a9f7-2260-4bc2-a54b-3d896acf4206 -&amp;gt; http://localhost:4000
Starting webhook relay agent...
2019-08-28 23:14:41.773 INFO    using standard transport...
2019-08-28 23:14:41.928 INFO    webhook relay ready...  &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;: &lt;span class="s2"&gt;"my.webhookrelay.com:8080"&lt;/span&gt;, &lt;span class="s2"&gt;"buckets"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"8e977e70-09a6-464c-ad30-855e1cd5d9f9"&lt;/span&gt;&lt;span class="o"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, bucket will be used later to subscribe to github requests while destination is just a mandatory argument that we don't have to use in this case.&lt;/p&gt;

&lt;p&gt;Grab that &lt;a href="https://my.webhookrelay.com/v1/webhooks/***"&gt;https://my.webhookrelay.com/v1/webhooks/***&lt;/a&gt; URL and go to your repository's settings -&amp;gt; webhooks section. Once there, set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payload URL to your unique &lt;a href="https://my.webhookrelay.com/v1/webhooks/***"&gt;https://my.webhookrelay.com/v1/webhooks/***&lt;/a&gt; URL&lt;/li&gt;
&lt;li&gt;Content type to &lt;em&gt;application/json&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Secret to a random secret name, for the sake of this example my secret will be 'webhooksecret'&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Let me select individual events.&lt;/code&gt; and select &lt;em&gt;Releases&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, go to your repository's releases page (for example &lt;a href="https://github.com/webhookrelay/docker-compose-update-on-git-push/releases"&gt;https://github.com/webhookrelay/docker-compose-update-on-git-push/releases&lt;/a&gt;) and make a new release &lt;code&gt;1.0.0&lt;/code&gt;. Then, if you visit bucket details page or &lt;a href="https://my.webhookrelay.com/logs"&gt;logs page&lt;/a&gt; - you should see webhook from Github. Open it and let's inspect the payload. It's quite lengthy but we should be able to see&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"released"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in the top. To ensure that we only react on these events, create a rule:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--U5rWFN3x--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/h81b4f6kj0p40gju4mbs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--U5rWFN3x--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/h81b4f6kj0p40gju4mbs.png" alt="release filter" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you tag another release, you should see now that only one webhook was forwarder&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lbZZmbzQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/vglu9whudofjj376jq84.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lbZZmbzQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/vglu9whudofjj376jq84.png" alt="rule stopped webhook" width="800" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Update script and starting relay background service
&lt;/h2&gt;

&lt;p&gt;Our update script is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

git pull
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will pull the latest compose file and update containers. Now, let's update configuration in &lt;code&gt;relay.yml&lt;/code&gt; file (&lt;a href="https://my.webhookrelay.com/tokens"&gt;access key &amp;amp; secret can be generated here&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;xxx&lt;/span&gt;     &lt;span class="c1"&gt;# your access key&lt;/span&gt;
&lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;xxx&lt;/span&gt;  &lt;span class="c1"&gt;# your access secret&lt;/span&gt;
&lt;span class="na"&gt;buckets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-compose-update-on-git-push&lt;/span&gt; &lt;span class="c1"&gt;# your bucket name where github webhooks are sent&lt;/span&gt;
&lt;span class="na"&gt;relayer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exec&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
  &lt;span class="na"&gt;commandArgs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/full/path/to/docker-compose-update-on-git-push/update.sh&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;-- should be full path to your update script&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To start the relay, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;relay run &lt;span class="nt"&gt;-c&lt;/span&gt; relay.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will be running it through your terminal. For production use cases, please use &lt;a href="https://dev.to/v1/installation/background-service"&gt;background service mode&lt;/a&gt;. It will ensure that the daemon is launched on OS startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's try it out
&lt;/h2&gt;

&lt;p&gt;Launch docker-compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker ps
CONTAINER ID        IMAGE                           COMMAND                  CREATED             STATUS              PORTS                    NAMES
26cd2219e18b        redis:alpine                    &lt;span class="s2"&gt;"docker-entrypoint.s…"&lt;/span&gt;   20 seconds ago      Up 3 seconds        6379/tcp                 docker-compose-update-on-git-push_redis_1
63c8cd1ae7bb        karolisr/python-counter:0.1.0   &lt;span class="s2"&gt;"flask run"&lt;/span&gt;              20 seconds ago      Up 18 seconds       0.0.0.0:5000-&amp;gt;5000/tcp   docker-compose-update-on-git-push_web_1
&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:5000
I have been seen 1 times.
&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:5000
I have been seen 2 times.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next step would be to build a new image &lt;code&gt;0.2.0&lt;/code&gt; and push it to the registry. Once it's available, we can update our github repository &lt;code&gt;docker-compose.yml&lt;/code&gt; and make a new release. For the sake of this example, let's do this through the Github UI. &lt;/p&gt;

&lt;p&gt;In a few seconds you should see a new container running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker ps
CONTAINER ID        IMAGE                           COMMAND                  CREATED             STATUS              PORTS                    NAMES
27b2542423ec        karolisr/python-counter:0.2.0   &lt;span class="s2"&gt;"flask run"&lt;/span&gt;              9 seconds ago       Up 7 seconds        0.0.0.0:5000-&amp;gt;5000/tcp   docker-compose-update-on-git-push_web_1
26cd2219e18b        redis:alpine                    &lt;span class="s2"&gt;"docker-entrypoint.s…"&lt;/span&gt;   10 minutes ago      Up 9 minutes        6379/tcp                 docker-compose-update-on-git-push_redis_1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Optional: Validating Github secret
&lt;/h2&gt;

&lt;p&gt;Webhook Relay output rules can also validate Github signature:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--01JXcFGE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/5k36auilqr3xk3yzn7ur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--01JXcFGE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/5k36auilqr3xk3yzn7ur.png" alt="webhook filter signature" width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will ensure that only webhooks signed by Github will be processed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative solutions
&lt;/h2&gt;

&lt;p&gt;I have somewhat successfully used these tools to update containers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/pyouroboros/ouroboros"&gt;https://github.com/pyouroboros/ouroboros&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/containrrr/watchtower"&gt;https://github.com/containrrr/watchtower&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both tools work if you have a simple setup that just needs to track &lt;code&gt;latest&lt;/code&gt; tags. However, if you have multiple containers with semver (or any other versioning mechanism) - those solutions do not work. That's why it's useful to have an either .env or docker-compose.yml config with image tags committed into git and synchronized into the host that runs the services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As with any code executed on your machine - you have to be careful when automating tasks. Webhook Relay will provide you with a unidirectional flow of webhooks into the machine. Your script/applications are on your machine and cannot be modified remotely through Webhook Relay. Coupled with authenticated webhook endpoints (you can configure it on a bucket level) or webhook payload checksum validation - you can build a secure update mechanism.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally released on &lt;a href="https://webhookrelay.com/blog/2019/09/02/docker-compose-update-on-github-webhooks/"&gt;https://webhookrelay.com/blog/2019/09/02/docker-compose-update-on-github-webhooks/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>automation</category>
      <category>devops</category>
      <category>git</category>
    </item>
    <item>
      <title>Using Google Firestore for a Golang backend application</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Mon, 26 Aug 2019 18:08:39 +0000</pubDate>
      <link>https://forem.com/webhookrelay/using-google-firestore-for-a-golang-backend-application-1dbm</link>
      <guid>https://forem.com/webhookrelay/using-google-firestore-for-a-golang-backend-application-1dbm</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fwmseu7hax0hsxl7erxpn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fwmseu7hax0hsxl7erxpn.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Usually, when I need a database I just pick &lt;a href="https://www.postgresql.org/" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt; or embedded key-value stores such as the excellent &lt;a href="https://github.com/etcd-io/bbolt" rel="noopener noreferrer"&gt;boltdb&lt;/a&gt;, &lt;a href="https://github.com/dgraph-io/badger" rel="noopener noreferrer"&gt;badger from dgraph&lt;/a&gt; or &lt;a href="https://redis.io/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt; (if I need a KV store but shared between several nodes). With flexibility comes the burden of maintenance and sometimes additional cost. In this article, we will explore a simple Golang backend service that will use Google Firestore as storage.&lt;/p&gt;

&lt;p&gt;When I started working on a simple project called &lt;a href="https://bin.webhookrelay.com/" rel="noopener noreferrer"&gt;bin.webhookrelay.com&lt;/a&gt;, I picked Badger as a key-value store, attached a persistent disk to a Kubernetes pod and launched it. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://bin.webhookrelay.com" rel="noopener noreferrer"&gt;bin.webhookrelay.com&lt;/a&gt; is a free service that allows you to capture webhook or API requests for testing purposes. It also lets you specify what response body and status code to return, as well as set an optional response delay.  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The data model was (and still is) simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bin&lt;/strong&gt; that has a certain configuration like what status code to return, response body, content-type header, and response delay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request&lt;/strong&gt; which is the actual captures webhook request with bin ID, request body, headers, and query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the time, KV stores such as boltdb or badger are great for such use cases. Problems arise when you want to either scale horizontally or have a rolling update strategy meaning that a number of instances of your application would have to surge during the update. While Kubernetes is great for running pretty much any workload, an update where it has to detach a persistent disk and reattach it to a new pod can lead to downtime and just generally slow updates. I always try to avoid such scenarios, however, webhook bin service was suffering from it.&lt;/p&gt;

&lt;p&gt;This time, I decided to try out &lt;a href="https://firebase.google.com/docs/firestore/" rel="noopener noreferrer"&gt;Cloud Firestore&lt;/a&gt;. You probably have already heard about Firestore (previously known as Firebase) and that it is very popular amongst mobile app developers who need to have a database for their Android and iOS apps. Apparently, it can also provide a really nice developer UX for backend applications! :) &lt;/p&gt;

&lt;h2&gt;
  
  
  Setting it up
&lt;/h2&gt;

&lt;p&gt;For authentication, Golang Firestore client uses a standard mechanism that relies on a service account. Basically, you need to go to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://console.cloud.google.com" rel="noopener noreferrer"&gt;GCP console&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Click on IAM &amp;amp; admin&lt;/li&gt;
&lt;li&gt;Go to service accounts (on the left navigation bar) &lt;/li&gt;
&lt;li&gt;Create a new service account with Firestore permissions&lt;/li&gt;
&lt;li&gt;Download the file, you can now use it for authentication&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Docs can be found &lt;a href="https://cloud.google.com/docs/authentication/getting-started" rel="noopener noreferrer"&gt;in the Google Cloud authentication section&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Application code is surprisingly simple. You get the client using Google application credentials, project ID and that's it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewFirestoreBinManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;FirestoreBinManagerOpts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;FirestoreBinManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientOption&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CredsFile&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;    
        &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithCredentialsFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CredsFile&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;// credentials file option is optional, by default it will use GOOGLE_APPLICATION_CREDENTIALS&lt;/span&gt;
  &lt;span class="c"&gt;// environment variable, this is a default method to connect to Google services&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;firestore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProjectID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;FirestoreBinManager&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;binsCollection&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BinsCollection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c"&gt;// our bins collection name&lt;/span&gt;
        &lt;span class="n"&gt;reqsCollection&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReqsCollection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c"&gt;// our requests collection name&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pubsub&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pubsub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding and updating bins
&lt;/h2&gt;

&lt;p&gt;When creating a document, you can specify document ID and just pass in the whole golang struct without first marshaling it into JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;FirestoreBinManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;BinPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;binsCollection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetId&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that we supply collection name: &lt;code&gt;Collection(m.binsCollection)&lt;/code&gt;, ID: &lt;code&gt;Doc(b.GetId())&lt;/code&gt; and set the struct fields &lt;code&gt;Set(ctx, b)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This really saves time! An alternative with a KV store would be something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;BinManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;BinPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="n"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"bins/"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="c"&gt;// store package&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;badger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Txn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Your code here…&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deleting
&lt;/h2&gt;

&lt;p&gt;To delete a bin in our case means deleting both the bin document and all associated webhook requests with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;FirestoreBinManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;BinDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;binID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;binsCollection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to delete bin doc by ref"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Now, get all the requests and delete them in a batch request&lt;/span&gt;
    &lt;span class="n"&gt;iter&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reqsCollection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"=="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;binID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;numDeleted&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Batch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to iterate: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;numDeleted&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// If there are no documents to delete,&lt;/span&gt;
    &lt;span class="c"&gt;// the process is over.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;numDeleted&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;While it is very easy to store, retrieve and modify documents, some people will miss SQL type queries that can aggregate, count records and do other useful operations in the database. For example, to track document counts, you will have to implement a solution &lt;a href="https://cloud.google.com/firestore/docs/solutions/counters" rel="noopener noreferrer"&gt;similar to one described here&lt;/a&gt;. My suggestion would be to spend more time planning data structure and what kind of operations are you planning to use before embarking on this journey :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While being a bit skeptical at first, I quickly started liking Firestore. While running a managed Postgres would allow me to easier switch cloud providers, it would also make it more expensive to run. Keeping storage interface small means that you can implement Postgres (or any other database) driver in a matter of hours so then the most important things to look for are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much maintenance the solution requires&lt;/li&gt;
&lt;li&gt;Cost&lt;/li&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/GoogleCloudPlatform/golang-samples/tree/master/firestore/firestore_quickstart" rel="noopener noreferrer"&gt;Golang Firestore quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/GoogleCloudPlatform/golang-samples/tree/master/firestore/firestore_snippets" rel="noopener noreferrer"&gt;Golang Firestore snippets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/docs/authentication/getting-started" rel="noopener noreferrer"&gt;GCP Authentication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://firebase.google.com/docs/firestore/pricing" rel="noopener noreferrer"&gt;Firestore Billing&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://webhookrelay.com/blog/2019/08/26/using-google-firestore-for-go-backend/" rel="noopener noreferrer"&gt;Webhook Relay blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>beginners</category>
      <category>programming</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Automated Jenkins builds on GitHub pull request</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Wed, 17 Apr 2019 14:34:50 +0000</pubDate>
      <link>https://forem.com/webhookrelay/automated-jenkins-builds-on-github-pull-request-4lh2</link>
      <guid>https://forem.com/webhookrelay/automated-jenkins-builds-on-github-pull-request-4lh2</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fodztjm11zvoym0a21kte.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fodztjm11zvoym0a21kte.png" alt="workflow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this short guide we will configure Jenkins to start builds on GitHub pull requests. Subsequent builds will be triggered on any new commits and GitHub pull request status will show whether build succeeded or failed. This setup will work without configuring router, firewall or having a public IP. It will also work behind a corporate firewall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a VM
&lt;/h2&gt;

&lt;p&gt;In my case, I just grabbed a Vagrant box from &lt;a href="https://app.vagrantup.com/ubuntu/boxes/xenial64:" rel="noopener noreferrer"&gt;https://app.vagrantup.com/ubuntu/boxes/xenial64:&lt;/a&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"
  config.vm.network "private_network", type: "dhcp"
end


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

vagrant up
vagrant ssh


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And we have our VM. You can get the IP address by typing &lt;code&gt;ifconfig&lt;/code&gt; in the terminal. &lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Jenkins
&lt;/h2&gt;

&lt;p&gt;I mostly followed this guide &lt;a href="https://linuxize.com/post/how-to-install-jenkins-on-ubuntu-18-04/" rel="noopener noreferrer"&gt;https://linuxize.com/post/how-to-install-jenkins-on-ubuntu-18-04/&lt;/a&gt;. The only caveat I encountered this time with Jenkins, was the &lt;a href="https://stackoverflow.com/questions/39621263/jenkins-fails-when-running-service-start-jenkins" rel="noopener noreferrer"&gt;jdk version mismatch&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting to Jenkins
&lt;/h2&gt;

&lt;p&gt;First, get your Jenkins token:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; /var/lib/jenkins/secrets/initialAdminPassword
ce04d19270934633a7badcac3cfac316


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then, either open your node firewall (or check Vagrant port forwarding) &lt;strong&gt;or&lt;/strong&gt; do the easy thing: connect with the relay:&lt;/p&gt;

&lt;h4&gt;
  
  
  Get CLI
&lt;/h4&gt;

&lt;p&gt;To get the CLI, check &lt;a href="https://webhookrelay.com/v1/installation/cli" rel="noopener noreferrer"&gt;instructions here&lt;/a&gt;. On a 64-bit Linux OS it's:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://storage.googleapis.com/webhookrelay/downloads/relay-linux-amd64 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; relay &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod&lt;/span&gt; +wx relay &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo mv &lt;/span&gt;relay /usr/local/bin


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
  
  
  Login
&lt;/h4&gt;

&lt;p&gt;Go to &lt;a href="https://my.webhookrelay.com/tokens" rel="noopener noreferrer"&gt;https://my.webhookrelay.com/tokens&lt;/a&gt;, click &lt;strong&gt;CREATE TOKEN&lt;/strong&gt; and copy/paste login command into the terminal, it should be something like:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

relay login &lt;span class="nt"&gt;-k&lt;/span&gt; &amp;lt;your key&amp;gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;your secret&amp;gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
  
  
  Start tunnel
&lt;/h4&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;relay connect :8080
Connecting: 
http://lsw7eq49jlhsuldvhpiyku.webrelay.io &amp;lt;&lt;span class="nt"&gt;----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; http://127.0.0.1:8080
https://lsw7eq49jlhsuldvhpiyku.webrelay.io &amp;lt;&lt;span class="nt"&gt;----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; http://127.0.0.1:8080


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now, open the browser. You should see a similar screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiks2ptqh44g5n62e2776.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiks2ptqh44g5n62e2776.png" alt="Jenkins login view"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Follow the steps to configure your Jenkins initial admin user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the plugin
&lt;/h2&gt;

&lt;p&gt;Plugin installation instructions &lt;a href="https://wiki.jenkins.io/display/JENKINS/GitHub+pull+request+builder+plugin" rel="noopener noreferrer"&gt;can be found here&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Once you have it, add GitHub credentials - your username and GitHub token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forwarding configuration
&lt;/h2&gt;

&lt;p&gt;Go to your &lt;a href="https://my.webhookrelay.com/buckets" rel="noopener noreferrer"&gt;bucket configuration&lt;/a&gt; and create a bucket called &lt;code&gt;github-webhooks&lt;/code&gt;. Configure it to forward all webhooks to &lt;a href="http://localhost:8080/" rel="noopener noreferrer"&gt;http://localhost:8080/&lt;/a&gt;. This will ensure that webhooks will reach Jenkins server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptf753xi2e5zkmzbzm20.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptf753xi2e5zkmzbzm20.png" alt="forwarding config"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you have the &lt;code&gt;relay&lt;/code&gt; CLI on the machine where you run Jenkins, type:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

relay forward &lt;span class="nt"&gt;--bucket&lt;/span&gt; github-webhooks


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This will start forwarding webhooks. There are alternative options to run the forwarding daemon, such as &lt;a href="https://webhookrelay.com/v1/installation/docker.html" rel="noopener noreferrer"&gt;Docker container&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you are creating webhook configuration manually in GitHub, use &lt;a href="http://localhost:8080/ghprbhook" rel="noopener noreferrer"&gt;http://localhost:8080/ghprbhook&lt;/a&gt; destination as it's the endpoint on which the plugin is listening for webhooks. In default case, Jenkins will automatically transform &lt;a href="https://my.webhookrelay.com/v1/webhooks/21e13033-bd3d-47a2-bf15-6fd42d4b40a3" rel="noopener noreferrer"&gt;https://my.webhookrelay.com/v1/webhooks/21e13033-bd3d-47a2-bf15-6fd42d4b40a3&lt;/a&gt; endpoints to &lt;a href="https://my.webhookrelay.com/v1/webhooks/21e13033-bd3d-47a2-bf15-6fd42d4b40a3/ghprbhook" rel="noopener noreferrer"&gt;https://my.webhookrelay.com/v1/webhooks/21e13033-bd3d-47a2-bf15-6fd42d4b40a3/ghprbhook&lt;/a&gt; and &lt;br&gt;
Webhook Relay will preserve the extra &lt;code&gt;/ghprbhook&lt;/code&gt; path.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, configure &lt;strong&gt;GitHub Pull Request Builder&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk6tplkx0cka0pdf4s07k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk6tplkx0cka0pdf4s07k.png" alt="pr builder config"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a job
&lt;/h2&gt;

&lt;p&gt;To create a new job, first select "Freestyle project", then:&lt;/p&gt;

&lt;p&gt;Add the project's GitHub URL to the "GitHub project" field (the one you can enter into browser. eg: "&lt;a href="https://github.com/rusenask/jenkins-test/%22):" rel="noopener noreferrer"&gt;https://github.com/rusenask/jenkins-test/"):&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfw6zcdtgpz2szum9agt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfw6zcdtgpz2szum9agt.png" alt="jenkins set github project"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configure &lt;strong&gt;Source Code Management&lt;/strong&gt; section:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Select Git SCM.&lt;/li&gt;
&lt;li&gt;Add your GitHub "Repository URL".&lt;/li&gt;
&lt;li&gt;Under Advanced, set "refspec" to &lt;code&gt;+refs/pull/*:refs/remotes/origin/pr/*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In "Branch Specifier", enter &lt;code&gt;${ghprbActualCommit}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fespfb7kzrdxf60jk2oak.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fespfb7kzrdxf60jk2oak.png" alt="jenkins set source code management"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configure &lt;strong&gt;Build Triggers&lt;/strong&gt; with a list of admins and tick the &lt;code&gt;use github hooks for build triggering&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8zztchxck2iezq2wnlql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8zztchxck2iezq2wnlql.png" alt="jenkins build triggers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add your &lt;strong&gt;Build&lt;/strong&gt; step configuration. This can be anything you want, usually people tend to use either a bash script, Makefile target or something specific to your programming language such as &lt;code&gt;go build&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw5lx9n98gsc67r25edr1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw5lx9n98gsc67r25edr1.png" alt="jenkins build triggers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Opening a PR
&lt;/h2&gt;

&lt;p&gt;Now, whenever you open a new pull request in GitHub, you should see a build being triggered:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flyghperx7e3aqs8s9wap.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flyghperx7e3aqs8s9wap.png" alt="pr triggering build"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can view build status in your Jenkins instance as well. This build indicator in GitHub will either turn red or green based on the build status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As we can see, there are several required steps to make sure your PRs get automatically built and tested when using Jenkins. Those can be split into two groups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;System configuration that involves:

&lt;ul&gt;
&lt;li&gt;setting Webhook Relay agent to forwarding webhooks&lt;/li&gt;
&lt;li&gt;installing and configuring &lt;a href="https://wiki.jenkins.io/display/JENKINS/GitHub+pull+request+builder+plugin" rel="noopener noreferrer"&gt;GitHub pull request builder plugin&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Whenever you create a new project in Jenkins, setting up few settings. The first time I did this it took me some time to go through the configuration options, but second and third time didn't take more than 1 minute :)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I hope you will find this guide useful!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Bonus troubleshooting below:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When we are talking about Jenkins, there are many ways for things to go wrong. Multiple plugin versions, corporate proxies and different operating systems contribute to all of this. I have compiled a short list of items for you to check if you encounter problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ensuring webhook configuration
&lt;/h3&gt;

&lt;p&gt;Make sure there's an automatically created GitHub repository webhook configuration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff41eayc2owxuj0b3ewwx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff41eayc2owxuj0b3ewwx.png" alt="GitHub repo webhook configuration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Ensuring Webhook Relay agent can connect
&lt;/h3&gt;

&lt;p&gt;Normally, connected agent should look like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

relay forward &lt;span class="nt"&gt;--bucket&lt;/span&gt; github-webhooks
Filtering on bucket: github-webhooks
Starting webhook relay agent... 
1.55523552627511e+09    info    using standard transport...
1.5552355264042027e+09  info    webhook relay ready...  &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;: &lt;span class="s2"&gt;"my.webhookrelay.com:8080"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If you are behind a corporate proxy, try adding &lt;code&gt;--ws&lt;/code&gt; flag to change default transport type from GRPC to WebSocket:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

relay forward &lt;span class="nt"&gt;--ws&lt;/span&gt; &lt;span class="nt"&gt;--bucket&lt;/span&gt; github-webhooks
Filtering on bucket: github-webhooks
Starting webhook relay agent... 
1.5552387754607065e+09  info    using websocket based transport...
1.5552387754607568e+09  info    authenticating to &lt;span class="s1"&gt;'wss://my.webhookrelay.com:443/v1/socket'&lt;/span&gt;...
1.5552387754608495e+09  info    websocket reader process started...
1.555238775470567e+09   info    subscribing to buckets: &lt;span class="o"&gt;[&lt;/span&gt;github-webhooks


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Check logs
&lt;/h3&gt;

&lt;p&gt;There will be several sources of logs you can check out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jenkins logs under &lt;code&gt;/log/all&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhre1kx43pt290xo7d49u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhre1kx43pt290xo7d49u.png" alt="jenkins logs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Webhook Relay forwarding logs:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkv5xxofnc68yxixlg29a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkv5xxofnc68yxixlg29a.png" alt="forwarding logs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CLI logs:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

relay forward &lt;span class="nt"&gt;--bucket&lt;/span&gt; github-webhooks
Filtering on bucket: github-webhooks
Starting webhook relay agent... 
1.55523552627511e+09    info    using standard transport...
1.5552355264042027e+09  info    webhook relay ready...  &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;: &lt;span class="s2"&gt;"my.webhookrelay.com:8080"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
1.555236773074343e+09   info    webhook request relayed &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"destination"&lt;/span&gt;: &lt;span class="s2"&gt;"http://localhost:8080/ghprbhook/"&lt;/span&gt;, &lt;span class="s2"&gt;"method"&lt;/span&gt;: &lt;span class="s2"&gt;"POST"&lt;/span&gt;, &lt;span class="s2"&gt;"bucket"&lt;/span&gt;: &lt;span class="s2"&gt;"github-webhooks"&lt;/span&gt;, &lt;span class="s2"&gt;"status"&lt;/span&gt;: 200, &lt;span class="s2"&gt;"retries"&lt;/span&gt;: 0&lt;span class="o"&gt;}&lt;/span&gt;
1.5552368184301443e+09  info    webhook request relayed &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"destination"&lt;/span&gt;: &lt;span class="s2"&gt;"http://localhost:8080/ghprbhook/"&lt;/span&gt;, &lt;span class="s2"&gt;"method"&lt;/span&gt;: &lt;span class="s2"&gt;"POST"&lt;/span&gt;, &lt;span class="s2"&gt;"bucket"&lt;/span&gt;: &lt;span class="s2"&gt;"github-webhooks"&lt;/span&gt;, &lt;span class="s2"&gt;"status"&lt;/span&gt;: 200, &lt;span class="s2"&gt;"retries"&lt;/span&gt;: 0&lt;span class="o"&gt;}&lt;/span&gt;
1.5552368215106862e+09  info    webhook request relayed &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"destination"&lt;/span&gt;: &lt;span class="s2"&gt;"http://localhost:8080/ghprbhook/"&lt;/span&gt;, &lt;span class="s2"&gt;"method"&lt;/span&gt;: &lt;span class="s2"&gt;"POST"&lt;/span&gt;, &lt;span class="s2"&gt;"bucket"&lt;/span&gt;: &lt;span class="s2"&gt;"github-webhooks"&lt;/span&gt;, &lt;span class="s2"&gt;"status"&lt;/span&gt;: 200, &lt;span class="s2"&gt;"retries"&lt;/span&gt;: 0&lt;span class="o"&gt;}&lt;/span&gt;
1.555236829308788e+09   info    webhook request relayed &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"destination"&lt;/span&gt;: &lt;span class="s2"&gt;"http://localhost:8080/ghprbhook/"&lt;/span&gt;, &lt;span class="s2"&gt;"method"&lt;/span&gt;: &lt;span class="s2"&gt;"POST"&lt;/span&gt;, &lt;span class="s2"&gt;"bucket"&lt;/span&gt;: &lt;span class="s2"&gt;"github-webhooks"&lt;/span&gt;, &lt;span class="s2"&gt;"status"&lt;/span&gt;: 200, &lt;span class="s2"&gt;"retries"&lt;/span&gt;: 0&lt;span class="o"&gt;}&lt;/span&gt;
1.5552368314174337e+09  info    webhook request relayed &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"destination"&lt;/span&gt;: &lt;span class="s2"&gt;"http://localhost:8080/ghprbhook/"&lt;/span&gt;, &lt;span class="s2"&gt;"method"&lt;/span&gt;: &lt;span class="s2"&gt;"POST"&lt;/span&gt;, &lt;span class="s2"&gt;"bucket"&lt;/span&gt;: &lt;span class="s2"&gt;"github-webhooks"&lt;/span&gt;, &lt;span class="s2"&gt;"status"&lt;/span&gt;: 200, &lt;span class="s2"&gt;"retries"&lt;/span&gt;: 0&lt;span class="o"&gt;}&lt;/span&gt;
1.555236920064973e+09   info    webhook request relayed &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"destination"&lt;/span&gt;: &lt;span class="s2"&gt;"http://localhost:8080/ghprbhook/"&lt;/span&gt;, &lt;span class="s2"&gt;"method"&lt;/span&gt;: &lt;span class="s2"&gt;"POST"&lt;/span&gt;, &lt;span class="s2"&gt;"bucket"&lt;/span&gt;: &lt;span class="s2"&gt;"github-webhooks"&lt;/span&gt;, &lt;span class="s2"&gt;"status"&lt;/span&gt;: 200, &lt;span class="s2"&gt;"retries"&lt;/span&gt;: 0&lt;span class="o"&gt;}&lt;/span&gt;
1.5552369202506151e+09  info    webhook request relayed &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"destination"&lt;/span&gt;: &lt;span class="s2"&gt;"http://localhost:8080/ghprbhook/"&lt;/span&gt;, &lt;span class="s2"&gt;"method"&lt;/span&gt;: &lt;span class="s2"&gt;"POST"&lt;/span&gt;, &lt;span class="s2"&gt;"bucket"&lt;/span&gt;: &lt;span class="s2"&gt;"github-webhooks"&lt;/span&gt;, &lt;span class="s2"&gt;"status"&lt;/span&gt;: 200, &lt;span class="s2"&gt;"retries"&lt;/span&gt;: 0&lt;span class="o"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Originally published on Webhook Relay blog: &lt;a href="https://webhookrelay.com/blog/2019/04/17/automated-github-pull-request-builds-on-jenkins/" rel="noopener noreferrer"&gt;https://webhookrelay.com/blog/2019/04/17/automated-github-pull-request-builds-on-jenkins/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>productivity</category>
      <category>git</category>
      <category>jenkins</category>
    </item>
    <item>
      <title>Automating testing, building and publishing of TypeScript libraries</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Sun, 17 Feb 2019 12:36:00 +0000</pubDate>
      <link>https://forem.com/webhookrelay/automating-testing-building-and-publishing-of-typescript-libraries-5652</link>
      <guid>https://forem.com/webhookrelay/automating-testing-building-and-publishing-of-typescript-libraries-5652</guid>
      <description>&lt;p&gt;It doesn't matter whether you are working on a side project, small open source library or your full-time job project, automating builds, tests and releases can greatly improve your life. You can then concentrate on code quality, features or just have a small break when you finish a task instead of trying to remember all the required steps to make a release. &lt;/p&gt;

&lt;p&gt;In my previous article I demonstrated how to set up a &lt;a href="https://dev.to/krusenas/setting-up-simple-self-hosted--fast-cicd-solution-with-drone-for-homelab-or-office-1fnd"&gt;self-hosted CI/CD solution with Drone&lt;/a&gt;. You don't need a powerful CI server or expensive VMs to run it, you can easily get one running on your laptop to perform these tasks in the background a lot faster than the free alternatives while also getting much greater flexibility. &lt;/p&gt;

&lt;p&gt;Now, I would like to share some practical pipelines that I have recently implemented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A short disclaimer:&lt;/strong&gt; I don't identify myself as an experienced TypeScript/JavaScript developer, I always lean to Go but in this case I needed to write some JavaScript so it was a great opportunity to finally try out TypeScript :) The package itself can be found &lt;a href="https://github.com/webhookrelay/webhookrelay-ws-client"&gt;here&lt;/a&gt;, it's a simple library that allows you to receive webhooks inside your app without exposing it to the internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the library
&lt;/h2&gt;

&lt;p&gt;My library tests were probably not what you find in a standard library. Since they rely on the SaaS service (to receive those public webhooks), it has to get some credentials from environment and perform asynchronous actions. That's where I learnt about &lt;code&gt;done&lt;/code&gt; callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should be able to forward the webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payload-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// creating a handler&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscribed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;---- once received, send a webhook&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;dispatchWebhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://my.webhookrelay.com/v1/webhooks/9c1f0997-1a34-4357-8a88-87f604daeca9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;          
            &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dispatchWebhook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;---- once webhook received, end the test case&lt;/span&gt;
        &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;WebhookRelayClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;testBucket&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;---- connecting so our handler will be called&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this not really related to automation, it might be useful for someone :) &lt;/p&gt;

&lt;h2&gt;
  
  
  Building the library
&lt;/h2&gt;

&lt;p&gt;When using Drone, everything runs in a Docker container. The main benefit of this is that it becomes trivial to get a reproducible builds. In our case, the first step includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install dependencies&lt;/li&gt;
&lt;li&gt;Build with &lt;code&gt;tsc&lt;/code&gt; (TypeScript needs to be converted back to JavaScript)&lt;/li&gt;
&lt;li&gt;Run tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our Drone file looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:latest&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# supplying environment variables for testing&lt;/span&gt;
    &lt;span class="na"&gt;RELAY_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;from_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;relay_key&lt;/span&gt;
    &lt;span class="na"&gt;RELAY_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;from_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;relay_secret&lt;/span&gt;
    &lt;span class="na"&gt;RELAY_BUCKET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ws-client-tests&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm install&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make test&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;here, &lt;code&gt;npm run build&lt;/code&gt; is actually just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scripts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;build&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tsc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the Makefile &lt;code&gt;make test&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test:
    ./node_modules/mocha/bin/mocha --reporter spec --compilers ts:ts-node/register src/*.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Publishing to npm registry
&lt;/h2&gt;

&lt;p&gt;It's always good to automate publishing packages as well. This way you will get a good release process for almost zero effort. When you are happy with the package functionality, you just tag a Github release and Drone will build, test and publish your package to npm registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publish&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:latest&lt;/span&gt;  
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;NPM_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;from_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm_token&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make drone-publish&lt;/span&gt;
  &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;tag&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;environment variable &lt;code&gt;NPM_TOKEN&lt;/code&gt; is a token that you can generate for your account. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;make drone-publish&lt;/code&gt; command looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;drone-publish:
    echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" &amp;gt; ~/.npmrc 
    npm publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is important to set that &lt;code&gt;.npmrc&lt;/code&gt; file as the publishing won't work without it. Strange? yes. &lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Notifications
&lt;/h2&gt;

&lt;p&gt;This last step is repeated across all my Drone pipelines, it's a notification to a Slack channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slack&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plugins/slack&lt;/span&gt;
  &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;success&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;failure&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;webhook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;from_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slack_url&lt;/span&gt;
    &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;general&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this to work, get your Slack's webhook URL and create a &lt;code&gt;slack_url&lt;/code&gt; secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;It takes 30-90 minutes to set up everything initially and once you have a CI system running, subsequent repositories can be added in seconds. Even if you think that running &lt;code&gt;npm run build&lt;/code&gt; and &lt;code&gt;npm publish&lt;/code&gt; takes only 1 minute of your time every time you release, automating this process will greatly improve your developer experience and life in general :) Combining automated builds and releases with tests will ensure that there is only one path to getting your package published. I have seen many cases where a TypeScript package build step was missed and the previous version was released. Or, after a 'quick fix' and push to registry the package was broken because someone forgot to run the test. Or, just consider that in the next year you might do 200 releases that would end up in many hours saved by automation!  &lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Setting up simple, self-hosted &amp; fast CI/CD solution with Drone for homelab or office</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Mon, 11 Feb 2019 18:37:55 +0000</pubDate>
      <link>https://forem.com/webhookrelay/setting-up-simple-self-hosted--fast-cicd-solution-with-drone-for-homelab-or-office-1fnd</link>
      <guid>https://forem.com/webhookrelay/setting-up-simple-self-hosted--fast-cicd-solution-with-drone-for-homelab-or-office-1fnd</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjv613cowmr9vzhn5akjx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjv613cowmr9vzhn5akjx.png" alt="Creating Drone tunnel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Continuous Integration and Continuous Delivery have been very trendy topics in the DevOps world for the last several years. As more and more organizations strive to improve their test and release processes, we have observed many new tools appear in this ecosystem. All major cloud providers are offering their automation systems (&lt;a href="https://toxicbakery.github.io/vsts-devops/microsoft-devops-ci/" rel="noopener noreferrer"&gt;some are very unsuccessful&lt;/a&gt;) so it's getting quite hard to pick the right one that fits your use-case. &lt;/p&gt;

&lt;p&gt;There are quite a lot of services (Travis, CircleCI, Google Cloud Builder) and self-hosted solutions to build, test and deploy your code, but actually, a few are free, open-source and self-hosted. &lt;/p&gt;

&lt;p&gt;The most well-know CI/CD tools are Jenkins and GitLab CI (I have recently asked this on &lt;a href="https://dev.to/krusenas/whats-your-favourite-cicd-tool-and-why-52ll"&gt;dev.to post&lt;/a&gt;). However, Jenkins has a huge memory footprint since it runs on Tomcat (Java) and the workflows are defined inside Jenkins itself, not in the repository that will be used to run those tests/build jobs.&lt;/p&gt;

&lt;p&gt;As for GitLab CI, it's great but requires you to run your own GitLab (which is huge) or to be on gitlab.com (although now you can mirror your Github repos to Gitlab to start builds automatically, however, it adds complexity). You can run your own runner independently though, they seem to be quite reliable. One of the organizations that I am helping with backend development is using Gitlab CI to great success, but as I mentioned before, business pricing and server running costs could be significant.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Little disclaimer: I have tried using Drone several years ago and I wasn't happy with it. Not sure whether it was the actual product fault or just documentation but I couldn't achieve what I wanted. This time seems very different. The product is stable, documentation is good and some very good examples are lying inside their own &lt;code&gt;.drone.yml&lt;/code&gt; file: &lt;a href="https://github.com/drone/drone/blob/master/.drone.yml" rel="noopener noreferrer"&gt;https://github.com/drone/drone/blob/master/.drone.yml&lt;/a&gt;. Builds are fast and reliable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Drone
&lt;/h2&gt;

&lt;p&gt;I have looked into many alternatives and decided that at least for me, the best CI/CD solution right now is &lt;a href="https://drone.io" rel="noopener noreferrer"&gt;Drone&lt;/a&gt;. It's an open source, very lightweight application written in Go that is available as a SaaS or as a self-hosted server. Drone consists of 2 components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server&lt;/strong&gt; is responsible for authentication, repository configuration, users, secrets and accepting webhooks. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt; are receiving build jobs and actually run your workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both server and agent are extremely lightweight services and only uses around ~10-15MB RAM. It basically means that you will not even feel that it's running on your laptop, desktop or even a Raspberry Pi. &lt;/p&gt;

&lt;h4&gt;
  
  
  My setup
&lt;/h4&gt;

&lt;p&gt;I am running Drone server on a Raspberry PI while the agent is running locally on my desktop which has lots of RAM and 16 CPU cores. This means that builds are a lot faster than any free public services could offer. What is more, since agents are connecting to the server over the Webhook Relay tunnels, I can deploy them on Kubernetes, spare laptops or other machines running anywhere:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvdjqtm5ikwkuy305mgb4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvdjqtm5ikwkuy305mgb4.png" alt="Drone server on RPI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Benefits of such configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No need to configure NAT, Drone server barely uses any resources so it can run on cheap hardware (hence Raspberry Pi with a bunch of other services).&lt;/li&gt;
&lt;li&gt;Agents are decoupled and can be running on your home/office machines. By connecting to them over the tunnels, agent configuration remains the same, no matter where they are deployed (external cloud provider or internal office).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Future plans include transitioning from Raspberry Pi for Drone server to an Intel NUC (or some other low power alternative) and running some additional Drone agents on the same server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing configuration
&lt;/h2&gt;

&lt;p&gt;Initial configuration is very straightforward. First, we create a tunnel on Webhook Relay to get our public subdomain. Then, we use that subdomain to create a Github OAuth app. &lt;/p&gt;

&lt;p&gt;Once the server and tunneling daemon are running, you can connect/disconnect Drone agents without any hassle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up the tunnel for Github and remote access
&lt;/h3&gt;

&lt;p&gt;I like dogfooding, therefore we are using our own tunnels throughout our stack (CI/CD, monitoring dashboards, various other automations). In this case, Webhook Relay tunnels provide the two most important things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access to Drone server no matter where it is (on your laptop, desktop or on some Raspberry Pi), no need to configure a firewall or router to get remote access.&lt;/li&gt;
&lt;li&gt;Webhooks from Github will trigger the workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go to your &lt;a href="https://my.webhookrelay.com/tunnels" rel="noopener noreferrer"&gt;tunnels page&lt;/a&gt; and create a new tunnel. If you are on a free tier, you will get a generated subdomain, it's fine. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn94443grucissjdg69zj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn94443grucissjdg69zj.png" alt="Creating Drone tunnel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;name of the tunnel&lt;/strong&gt; can be anything, but it has to match whatever you will set to the webhookrelayd container in the docker-compose.yaml. It acts as a filter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;subdomain&lt;/strong&gt; is your public subdomain under &lt;code&gt;*.webrelay.io&lt;/code&gt;. If you are on a free plan, this will be auto-generated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;destination&lt;/strong&gt; is the address of the drone-server container. Since docker-compose configures virtual networks, it has to match the service name from the docker-compose.yaml.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;crypto type&lt;/strong&gt; is available for all paid plans, basically HTTP/TLS. I would recommend subscribing to get encryption! Otherwise, ping me a message and I can set up a free trial.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuring Github OAuth application
&lt;/h3&gt;

&lt;p&gt;In this guide, we will be setting up Drone with Github. You can follow &lt;a href="https://docs.drone.io/installation/github/single-machine/" rel="noopener noreferrer"&gt;official docs&lt;/a&gt;. In short:&lt;/p&gt;

&lt;p&gt;Create a GitHub OAuth application. The Consumer Key and Consumer Secret are used to authorize access to Github resources. The Authorization callback URL must match the below format and path, and must use your exact server scheme and host. So if your Webhook Relay tunnel address is &lt;code&gt;my-drone-subdomain.webrelay.io&lt;/code&gt; then the Authorization callback URL will be &lt;code&gt;https://my-drone-subdomain.webrelay.io/login&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying Drone server &amp;amp; agent
&lt;/h3&gt;

&lt;p&gt;I have initially started using Drone with both server and agent running on my desktop since it was only for my personal use, it was fine. However, later I detached server and moved it into a Raspberry Pi. &lt;/p&gt;

&lt;p&gt;The all-in-one configuration looks like:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;drone-server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drone-server&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drone/drone:1.0.1&lt;/span&gt;   
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db-data:/var/lib/drone/&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# - DRONE_OPEN=false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_SERVER_HOST=my-drone-subdomain.webrelay.io&lt;/span&gt; &lt;span class="c1"&gt;# tunnel hostname       &lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_GITHUB_SERVER=https://github.com&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_ADMIN=&amp;lt;your username&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_GITHUB_CLIENT_ID=&amp;lt;github-client-id&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_GITHUB_CLIENT_SECRET=&amp;lt;github-client-secret&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_SERVER_PROTO=http&lt;/span&gt; &lt;span class="c1"&gt;# tunnel adds https on top&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_RPC_SECRET=&amp;lt;shared secret&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_USER_CREATE=username:&amp;lt;your github username&amp;gt;,admin:true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_AGENTS_ENABLED=true&lt;/span&gt;
  &lt;span class="na"&gt;drone-agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drone-agent&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drone/agent:1.0.1&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agent&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;drone-server&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_RPC_SERVER=https://my-drone-subdomain.webrelay.io&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_RPC_SECRET=&amp;lt;shared secret&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_RUNNER_CAPACITY=8&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_RUNNER_NAME="local"&lt;/span&gt;

  &lt;span class="na"&gt;webhookrelay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drone-webhookrelay&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webhookrelay/webhookrelayd:latest&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--mode&lt;/span&gt; 
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;tunnel&lt;/span&gt; 
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-t&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;drone&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;drone-server&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KEY=&amp;lt;whr-key&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SECRET=&amp;lt;whr-secret&amp;gt;&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If you need another agent on a different machine, agent-only configuration:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;drone-agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drone-agent&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drone/agent:1.0.0-rc.5&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agent&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_RPC_SERVER=https://my-drone-subdomain.webrelay.io&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_RPC_SECRET=&amp;lt;shared secret&amp;gt;&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_RUNNER_CAPACITY=8&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DRONE_RUNNER_NAME="local"&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Once you have the config:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

docker-compose up


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;It will ask you to authenticate with your Github credentials. &lt;/p&gt;

&lt;h2&gt;
  
  
  Working with Drone pipelines
&lt;/h2&gt;

&lt;p&gt;One of the best things about Drone is their pipeline configuration. It seems like after the Drone showed the way, industry followed and now most of the CI/CD services implement the pipelines through the Docker containers. It means that your builds and tests are decoupled from physical hosts and the cleanup happens by design. An example pipeline, that&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;builds&lt;/li&gt;
&lt;li&gt;unit tests&lt;/li&gt;
&lt;li&gt;runs end-to-end tests with a Postgres database&lt;/li&gt;
&lt;li&gt;sends a webhook to Slack&lt;/li&gt;
&lt;li&gt;builds and pushes an image to DockerHub&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;looks like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;

&lt;span class="na"&gt;workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/go&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/github.com/webhookrelay/app&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golang:1.11.4&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cd cmd/app &amp;amp;&amp;amp; go build&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unit-test&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golang:1.11.4&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go test -v `go list ./... `&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;end-to-end&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golang:1.11.4&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    
    &lt;span class="na"&gt;POSTGRES_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;database&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pguser&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgpass&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgdb&lt;/span&gt;     
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make install-transponder&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make e2e&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slack&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plugins/slack&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;webhook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;from_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slack_url&lt;/span&gt;
    &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;general&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drone&lt;/span&gt;
    &lt;span class="na"&gt;icon_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://i.pinimg.com/originals/51/29/a4/5129a48ddad9e8408d2757dd10eb836f.jpg&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;  
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plugins/docker&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;namespace/repo&lt;/span&gt;
    &lt;span class="na"&gt;auto_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;from_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker_username&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;from_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker_password&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;database&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pguser&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgpass&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgdb&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;By always using containers to run your pipelines, you will make your builds and tests truly portable. Whenever you push to Github, a webhook will hit Drone and it will start a build:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcvz9ge93gqqur6j3x3l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcvz9ge93gqqur6j3x3l.png" alt="Drone pipeline"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips and tricks
&lt;/h2&gt;

&lt;p&gt;I would also like to share some very useful commands. To install &lt;code&gt;drone&lt;/code&gt; CLI, follow &lt;a href="https://docs.drone.io/cli/install/" rel="noopener noreferrer"&gt;official docs&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Executing pipelines directly
&lt;/h4&gt;

&lt;p&gt;You can also run pipelines directly with the Drone CLI:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

drone &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--secret-file&lt;/span&gt; drone_secrets.yaml .drone.yml


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A template for Drone secrets:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

&lt;p&gt;slack_url: &lt;a href="https://hooks.slack.com/services/xxxxxxxxxxxx" rel="noopener noreferrer"&gt;https://hooks.slack.com/services/xxxxxxxxxxxx&lt;/a&gt;&lt;/p&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Adding secrets through CLI&lt;br&gt;
&lt;/h4&gt;

&lt;p&gt;You can either add secrets through the web UI or use &lt;code&gt;drone&lt;/code&gt; directly from your terminal:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;p&gt;drone secret add &lt;span class="nt"&gt;-repository&lt;/span&gt; username/repository-name &lt;span class="nt"&gt;--name&lt;/span&gt; foo &lt;span class="nt"&gt;--data&lt;/span&gt; bar &lt;span class="nt"&gt;--allow-pull-request&lt;/span&gt;&lt;/p&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Wrapping up&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;I have used extensively Jenkins, Gitlab runners, CircleCI and Google Cloud Builder. I guess I still like a lot Cloud Builder for GKE related things as it builds images fast and authenticates to GCR registry without any additional work. However, you do get stuck with their pipeline configuration files and can't really move to any other vendor. As for Jenkins, it's just hard to imagine running it efficiently and smaller servers. Drone, at least for me, is an ideal example of a modern service that focuses on efficiency and doing one thing right.&lt;/p&gt;

&lt;p&gt;So, just to recap, main pros of Drone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Very easy to setup&lt;/li&gt;
&lt;li&gt;Out of the box it uses SQLite as a database but you can use Postgres or Mysql.&lt;/li&gt;
&lt;li&gt;Lightweight agents that can run anywhere&lt;/li&gt;
&lt;li&gt;Available either as a self-hosted solution or as a SaaS&lt;/li&gt;
&lt;li&gt;Pipelines defined as code&lt;/li&gt;
&lt;li&gt;Run pipelines locally with just drone CLI&lt;/li&gt;
&lt;li&gt;Integration with Github, Gitlab, Bitbucket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://webhookrelay.com/blog/2019/02/11/using-drone-for-simple-selfhosted-ci-cd/" rel="noopener noreferrer"&gt;Webhook Relay blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>productivity</category>
      <category>devops</category>
      <category>git</category>
    </item>
    <item>
      <title>What's your favourite CI/CD tool and why?</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Fri, 01 Feb 2019 16:06:01 +0000</pubDate>
      <link>https://forem.com/webhookrelay/whats-your-favourite-cicd-tool-and-why-52ll</link>
      <guid>https://forem.com/webhookrelay/whats-your-favourite-cicd-tool-and-why-52ll</guid>
      <description>&lt;p&gt;Throughout the years I have seen many companies using different tools to achieve same/similar things in CI/CD area. There's no denying that &lt;a href="https://jenkins.io/"&gt;Jenkins&lt;/a&gt; is probably the most widely used solution due to the huge amount of available integrations and plugins. There are, however, some downsides such as resource consumption that can become very expensive at scale. I always remember this great article from Monzo &lt;a href="https://monzo.com/blog/2016/09/19/building-a-modern-bank-backend/"&gt;Building a Modern Bank Backend&lt;/a&gt; that was mostly about migrating from Mesos to Kubernetes, but it does have some important points such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Previously, we ran several very beefy Jenkins hosts dedicated to the task, which were inefficient and expensive. Now, build jobs run under Kubernetes, using spare capacity in our existing infrastructure, which is basically free. - &lt;em&gt;Oliver Beattie&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Similar issues I have noticed in other companies, such as disks getting full due to growing workspaces or just agents getting quite slow due to insufficient host resources. Most of these issues could be labelled as user configuration problem, but then it seems that they don't exist in other systems. &lt;/p&gt;

&lt;p&gt;My favourite tools to automate tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/cloud-build/"&gt;Google Cloud Build&lt;/a&gt; - powerful and cheap alternative to CircleCI/Travis. If you are running on GCP, I would definitely recommend looking into it as for most users it will be running for free :0&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.gitlab.com/runner/"&gt;Gitlab CI/CD + runners&lt;/a&gt;. A great model, probably designed based on drone.io server/agent model. Nevertheless, works great most of the time :) &lt;/li&gt;
&lt;li&gt;
&lt;a href="//drone.io"&gt;Drone&lt;/a&gt; - very lightweight solution that you can run even on your desktop or on any server. Pipelines are defined in the same repo as your code and all builds happen in containers, so no issues with dirty workspaces or tying them to individual hosts. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It would be interesting to know what tools other people are using and why, please write your answer in the comments! :)&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Controlling gadgets with Google Home, IFTTT and Node-RED</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Tue, 29 Jan 2019 18:29:32 +0000</pubDate>
      <link>https://forem.com/webhookrelay/controlling-gadgets-with-google-home-ifttt-and-node-red-3ea2</link>
      <guid>https://forem.com/webhookrelay/controlling-gadgets-with-google-home-ifttt-and-node-red-3ea2</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7-BleiPB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/8nftvn44w3nuok1xjjyr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7-BleiPB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/8nftvn44w3nuok1xjjyr.png" alt="the flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a long time I have been using my Google Home mini just for setting up timers during cooking. Finally, I have found some more ways to use it! :) &lt;/p&gt;

&lt;p&gt;In this article, I will show you how easy and quick it is to add voice controlled commands to your home/office or any other environment. Once you set up your first flow, any other features on top of that won't take even 1 minute to add.&lt;/p&gt;

&lt;p&gt;We will be using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nodered.org/"&gt;Node-RED&lt;/a&gt; our main tool to wire everything together.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://assistant.google.com/"&gt;Google Home&lt;/a&gt; - I am using Google Home Mini to launch tasks.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ifttt.com/"&gt;IFTTT&lt;/a&gt; will be transforming commands coming from Google Home into webhooks.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://webhookrelay.com"&gt;Webhook Relay&lt;/a&gt; is going to act as a broker to deliver webhooks to our Raspberry PI running Node-RED without exposing it to the internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Webhook Relay node removes a lot of work that is required to securely expose your Node-RED to the internet. It is especially useful when you can't receive webhooks in your local network due to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ISP blocks incoming connections&lt;/li&gt;
&lt;li&gt;Double NAT due to using 4G&lt;/li&gt;
&lt;li&gt;No static IP&lt;/li&gt;
&lt;li&gt;Lack of knowledge how to set up HTTPS and reverse proxy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, it provides an encrypted, one-way transport to your Node-RED through a single node. And we do have a free tier.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you Node-RED is exposed to the internet, you can skip Webhook Relay part and just use HTTP node to accept the webhooks from IFTTT :) &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Preparing to receive webhooks from IFTTT
&lt;/h2&gt;

&lt;p&gt;Webhook Relay will be acting as a message broker between Google Home with IFTTT and Node-RED. Naturally, let's configure it first. Got to &lt;a href="https://my.webhookrelay.com/buckets"&gt;buckets page&lt;/a&gt; and create a new bucket called 'gactions':&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yAPyPfPT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/5ozm9jm8nlizdnlmeq9u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yAPyPfPT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/5ozm9jm8nlizdnlmeq9u.png" alt="Webhook Relay bucket"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the bucket details page, you should see 'Default public endpoint' URL that starts with &lt;code&gt;https://my.webhookrelay.com/v1/webhooks/...&lt;/code&gt;. Keep this tab open, you will need to copy that URL into IFTTT.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Set up Google Home with IFTTT
&lt;/h2&gt;

&lt;p&gt;Head to &lt;a href="https://ifttt.com/"&gt;IFTTT&lt;/a&gt;, then to &lt;a href="https://ifttt.com/my_applets"&gt;your applets&lt;/a&gt; and click on "New Applet". Search for "Google Assistant":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6F7IezEB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/byprnndjqkmw9evwlurm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6F7IezEB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/byprnndjqkmw9evwlurm.png" alt="IFTTT Google Assistant"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IF THIS (Google Assistant)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When choosing a trigger pick 'Say a simple phrase' for our scenario. You can try other ones later for different automation. Now, in 'What do you want to say?' section type 'turn the TV on' or something similar. Whatever you want basically. Populate other fields and pick your response phrase. Click "Create trigger".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;THEN THAT (Webhook)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the action service, choose webhook:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tPqdx2GU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/i9jlhwjwjyouozzr4yk9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tPqdx2GU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/i9jlhwjwjyouozzr4yk9.png" alt="IFTTT Google Assistant"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the URL part, take the 'Default public endpoint' from step 1 (that starts with &lt;code&gt;https://my.webhookrelay.com/v1/webhooks/...&lt;/code&gt;). Choose method to "POST", set Content Type to 'application/json' and set body to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tv_on"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Once done, click "Create Action". You can now repeat the same process for more commands such as turning the TV off, mute, lower the sound and so on. I have configured three applets in total to send webhooks to the same endpoint:&lt;/p&gt;

&lt;p&gt;To turn it off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tv_off"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;To mute it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tv_mute"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Adding new commands is very fast, takes less than a minute once you have finished with the first one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Configure Node-RED
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sRbb2w-q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/2ehhvd21ypskf19xb4z5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sRbb2w-q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/2ehhvd21ypskf19xb4z5.png" alt="Node-RED flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our Node-RED flow consists of three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive webhooks through &lt;a href="https://flows.nodered.org/node/node-red-contrib-webhookrelay"&gt;node-red-contrib-webhookrelay&lt;/a&gt; node.&lt;/li&gt;
&lt;li&gt;Extract body from the webhook and parse it using simple function and JSON node. &lt;/li&gt;
&lt;li&gt;Launch actions based on switch node.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To control TV I am using &lt;a href="https://flows.nodered.org/node/node-red-contrib-tv-bravia"&gt;node-red-contrib-tv-bravia&lt;/a&gt; node. Pretty much any internet connected device will also listen to &lt;a href="https://flows.nodered.org/node/node-red-node-wol"&gt;node-red-node-wol&lt;/a&gt; (Wake on LAN).&lt;/p&gt;

&lt;p&gt;The flow can be found on &lt;a href="https://gist.github.com/rusenask/c4686f64616efd2f73bbd1a8b9ecb0b0"&gt;GitHub gist here&lt;/a&gt;. You can either import it or add nodes one by one. For the learning purposes, I would advise to add them manually so you can better understand how it works.&lt;/p&gt;

&lt;p&gt;Let's get started. First, get &lt;a href="https://my.webhookrelay.com/tokens"&gt;authentication tokens&lt;/a&gt; and set them to the &lt;code&gt;node-red-contrib-webhookrelay&lt;/code&gt; node. In the "Buckets" field, add our "gactions" bucket which we created previously:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jPhs3H0e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/7slh0xzxpnjawytjyyov.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jPhs3H0e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/7slh0xzxpnjawytjyyov.png" alt="Webhook Relay node configuration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Extracting &amp;amp; parsing body
&lt;/h3&gt;

&lt;p&gt;Now, we need to extract body and parse it. Create a &lt;strong&gt;function&lt;/strong&gt; node and in the function body add this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This will extract the webhook body from the whole webhook message (it includes input, bucket metadata, as well as request method and headers). Then, add a &lt;strong&gt;json&lt;/strong&gt; node and configure it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Action:&lt;/strong&gt; Convert between JSON String &amp;amp; Object&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Property:&lt;/strong&gt; msg.payload&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Launching actions based on payload
&lt;/h3&gt;

&lt;p&gt;Finally, time to add the switch and main control nodes. Remember the payloads that we configured in IFTTT? Time to read that action value:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FH_kyJUg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/2wiya6pecds59vugh86o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FH_kyJUg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/2wiya6pecds59vugh86o.png" alt="switch node configuration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the control I am using &lt;a href="https://flows.nodered.org/node/node-red-contrib-tv-bravia"&gt;node-red-contrib-tv-bravia&lt;/a&gt; node. Follow their instructions to set up the TV. In short - you need to know the IP address of your TV and MAC (for the Wake on LAN node). You can either find it from your router or go to your TV network settings and pick it up from there. Each switch wire goes to a different action, this makes it simple and robust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future work
&lt;/h2&gt;

&lt;p&gt;Once you have the flow ready up to the &lt;strong&gt;switch&lt;/strong&gt; node, feel free to add more gadgets behind it. As you can see, IFTTT makes it super easy to issue commands and act based on those payloads in your flow. If you have your Node-RED exposed to the internet, you can even skip Webhook Relay node and receive webhooks directly through an &lt;code&gt;http&lt;/code&gt; node.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I have originally published this as a blog post here: &lt;a href="https://webhookrelay.com/blog/2019/01/29/google-home-ifttt-node-red/"&gt;https://webhookrelay.com/blog/2019/01/29/google-home-ifttt-node-red/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>nodered</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
