<?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: Conor Sheehan</title>
    <description>The latest articles on Forem by Conor Sheehan (@conorsheehan1).</description>
    <link>https://forem.com/conorsheehan1</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%2Fuser%2Fprofile_image%2F452105%2F7cd75138-4a47-49b3-a83e-fdb2c6e2ad31.png</url>
      <title>Forem: Conor Sheehan</title>
      <link>https://forem.com/conorsheehan1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/conorsheehan1"/>
    <language>en</language>
    <item>
      <title>Leaving Netlify Free Tier</title>
      <dc:creator>Conor Sheehan</dc:creator>
      <pubDate>Thu, 07 Mar 2024 00:00:00 +0000</pubDate>
      <link>https://forem.com/conorsheehan1/leaving-netlify-free-tier-1a4b</link>
      <guid>https://forem.com/conorsheehan1/leaving-netlify-free-tier-1a4b</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I’ve had 3 sites on netlify since 2022.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;Date created&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://spelling-b.netlify.app"&gt;https://spelling-b.netlify.app&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2022-08-15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://beach-litriochta.netlify.app"&gt;https://beach-litriochta.netlify.app&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2022-07-12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://conorscocktails.netlify.app"&gt;https://conorscocktails.netlify.app&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2022-01-06&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I liked the service, although I wasn’t thrilled with their default flow for deployments. I wrote a &lt;a href="https://conorsheehan1.github.io/blog/2022/02/21/netlify-deployments-from-github-without-giving-write-access.html"&gt;blogpost&lt;/a&gt; about how to deploy without giving netlify &lt;strong&gt;read &lt;em&gt;and&lt;/em&gt; write access&lt;/strong&gt; to all of your &lt;strong&gt;public &lt;em&gt;and&lt;/em&gt; private repositories&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leaving Netlify
&lt;/h2&gt;

&lt;p&gt;Then I read in the news last week that a random developer using the same supposedly free tier on netlify got hit with a &amp;gt;$100k bill due to a DDOS attack. &lt;a href="https://news.ycombinator.com/item?id=39520776"&gt;https://news.ycombinator.com/item?id=39520776&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is because the netlify free tier is not free. &lt;a href="https://www.netlify.com/pricing"&gt;https://www.netlify.com/pricing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Their landing page says &lt;code&gt;start for free&lt;/code&gt; but if you scroll down you’ll see there’s a &lt;code&gt;100GB bandwidth&lt;/code&gt; limit, and a fee for any traffic over the limit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PZuLAAfh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://conorsheehan1.github.io/assets/images/leaving_netlify/netlify-terms.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PZuLAAfh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://conorsheehan1.github.io/assets/images/leaving_netlify/netlify-terms.png" alt="netlify-terms" width="800" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Normally this would be fine, but when you hit their limit they don’t stop service. They might not even inform you that you’ve exceeded the free limit until you get a bill at the end of the month.&lt;/p&gt;

&lt;p&gt;They also do not protect against DDOS. They just continue service with their high rate of &lt;code&gt;$55 per 100GB&lt;/code&gt;. For comparison AWS is around &lt;code&gt;$9 per 100GB&lt;/code&gt;, and that’s still high IMO. &lt;a href="https://www.hostdime.com/blog/data-egress-fees-cloud"&gt;https://www.hostdime.com/blog/data-egress-fees-cloud&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’ve been stretched thin lately and not working on personal projects, but the thought of a potentially infinite bill definitely kicked me into gear. Last week I copied all my netlify sites to cloudflare pages.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://spelling-bee-free.pages.dev"&gt;https://spelling-bee-free.pages.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://beach-litriochta.pages.dev"&gt;https://beach-litriochta.pages.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://conors-cocktails.pages.dev"&gt;https://conors-cocktails.pages.dev&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Today I deleted all my netlify sites and my netlify account. I managed to run those 3 sites on netlify for the last 2 years with ~20Gb bandwidth usage, but I’m not willing to stay given the risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving to cloudflare pages
&lt;/h2&gt;

&lt;p&gt;I found the dev experience really good moving to &lt;a href="https://pages.cloudflare.com/"&gt;cloudflare pages&lt;/a&gt;. They give you preview sites based on pr builds for free. The github access is very easily controlled. They only ask for read access and you can choose the repositories they get access to. They do ask for write access to metadata so they can put status messages on PRs, but I like that.&lt;/p&gt;

&lt;p&gt;I only had 2 tiny issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I needed to set an environment variable &lt;code&gt;YARN_VERSION&lt;/code&gt; to get the build to work&lt;/li&gt;
&lt;li&gt;I had to delete and recreate a site to change the pages.dev url &lt;a href="https://developers.cloudflare.com/pages/platform/known-issues/#build-configuration]"&gt;https://developers.cloudflare.com/pages/platform/known-issues/#build-configuration&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;If you need to change your *.pages.dev subdomain, delete your project and create a new one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Other than that it was seamless. Definitely easier than the github action setup I needed for netlify.&lt;/p&gt;

&lt;p&gt;Most importantly, on the free plan if you exceed their limits, they shutdown the service instead of continuing and charging you! &lt;a href="https://developers.cloudflare.com/pages/functions/pricing/#free-plan"&gt;https://developers.cloudflare.com/pages/functions/pricing/#free-plan&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The icing on the cake is&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On both free and paid plans, requests to static assets are free and unlimited&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NNABTtl2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://conorsheehan1.github.io/assets/images/leaving_netlify/cloudflare-terms.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NNABTtl2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://conorsheehan1.github.io/assets/images/leaving_netlify/cloudflare-terms.png" alt="cloudflare-terms" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This reminds me of the good old days on heroku, where you had a free limit on compute each month, and if you exceeded it the service just stopped.&lt;/p&gt;

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

&lt;p&gt;Although the &lt;a href="https://news.ycombinator.com/item?id=39521986"&gt;Netlify CEO said they’d waive this bill&lt;/a&gt;, I wouldn’t recommend you host any site on netlify, unless you’re absolutely sure 100Gb of requests will make you more than netlify charge. For me I don’t run ads or make money per request, so I’ll be leaving entirely.&lt;/p&gt;

&lt;p&gt;It seems like this tactic of keeping service up no matter what and charging high fees later isn’t limited to netlify. Vercel do something similar. &lt;a href="https://serverlesshorrors.com/all/vercel-23k"&gt;https://serverlesshorrors.com/all/vercel-23k&lt;/a&gt;Generally I think this pattern is very dangerous. As a &lt;a href="https://news.ycombinator.com/item?id=39520981"&gt;commenter on hackernews pointed out&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It’s unbounded liability. Not to mention the strong conflict of interest for netlify, who stands to gain from their customers being attacked.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you’re looking for an alternative, I’d recommend &lt;a href="https://pages.cloudflare.com/"&gt;cloudflare pages&lt;/a&gt;. I found it great so far.&lt;/p&gt;

</description>
      <category>hosting</category>
      <category>netlify</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Buying And Configuring My First Domain</title>
      <dc:creator>Conor Sheehan</dc:creator>
      <pubDate>Thu, 04 Aug 2022 00:00:00 +0000</pubDate>
      <link>https://forem.com/conorsheehan1/buying-and-configuring-my-first-domain-53dm</link>
      <guid>https://forem.com/conorsheehan1/buying-and-configuring-my-first-domain-53dm</guid>
      <description>&lt;p&gt;Recently I made an Irish version of the New York Times &lt;a href="https://www.nytimes.com/puzzles/spelling-bee"&gt;spelling bee&lt;/a&gt; game. I wanted to host it on a domain that was easy to remember, but I’d never bought or configured a domain before. Here’s what I learned along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buying the domain
&lt;/h2&gt;

&lt;p&gt;First I had to choose a company to buy the domain from. I wanted a ‘.ie’ domain since it’s an Irish game, so that limited the options anyway. Initially, I was thinking of using &lt;a href="https://www.godaddy.com"&gt;GoDaddy&lt;/a&gt;, but they require a tax ID. The only Irish tax ID I know of is a PPSN, which you’re not meant to share with anyone but the government. After asking around on &lt;a href="https://www.reddit.com/r/DevelEire"&gt;r/DevelEire&lt;/a&gt; I decided to go with &lt;a href="https://www.blacknight.com"&gt;blacknight&lt;/a&gt;. It’s an Irish company so it was simpler to buy a ‘.ie’ domain from them, and they had similar prices anyway.&lt;/p&gt;

&lt;p&gt;Next, I had to choose a domain. I named the game &lt;a href="https://beach-litriochta.netlify.app"&gt;beach litríochta&lt;/a&gt;, which is a rough translation of spelling bee. Initially I wanted &lt;a href="https://beach.ie"&gt;https://beach.ie&lt;/a&gt; because it’d be super easy to remember, makes sense in both English and Irish, and is kind of funny since “Beach” means bee in Irish.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ad9xR1Fs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/beach_litriochta/beach_meme.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ad9xR1Fs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/beach_litriochta/beach_meme.webp" alt="beach-meme" width="500" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I thought the domain was free, but it just had a slow redirect. It resolves to &lt;a href="//ttps://beachawards.ie"&gt;https://beachawards.ie&lt;/a&gt;, so I couldn’t get it. Instead I went with &lt;a href="https://www.beacha.ie"&gt;https://www.beacha.ie&lt;/a&gt;. Beacha is the plural of beach, and it’s still short and hopefully easy to remember.&lt;/p&gt;

&lt;p&gt;Buying the domain was pretty straight forward. I had to upload a picture of my ID to blacknight to prove I’m an Irish citizen and that was it really.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the domain
&lt;/h2&gt;

&lt;p&gt;This is where I ran into some issues. Blacknight is changing it’s control panel, so a lot of its support forum FAQs won’t work for new customers. The old control panel is at &lt;a href="https://cp.blacknight.com"&gt;https://cp.blacknight.com&lt;/a&gt;, but I was a new customer so I had to use &lt;a href="https://cp.blacknighthosting.com"&gt;https://cp.blacknighthosting.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--teFInUrT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/beach_litriochta/blacknight_control_panel_update.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--teFInUrT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/beach_litriochta/blacknight_control_panel_update.png" alt="blacknight-control-panel" width="880" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I already had my game hosted with Netlify, so all I needed was to forward the traffic from my new domain to Netlify. I found this &lt;a href="https://help.blacknight.com/hc/en-us/articles/212523009-Domain-Forwarding"&gt;blacknight support article on domain forwarding&lt;/a&gt;, but it was using the old blacknight control panel so I couldn’t use it.&lt;/p&gt;

&lt;p&gt;I managed to get things working by following the &lt;a href="https://docs.netlify.com/domains-https/netlify-dns/delegate-to-netlify/"&gt;Netlify docs&lt;/a&gt;, some back and forth with blacknight support, and some trial and error. Here’s what I did to set up forwarding:&lt;/p&gt;

&lt;h3&gt;
  
  
  Forwarding traffic
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to Netlify and select your site -&amp;gt; site settings -&amp;gt; domain management &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hMKwitO0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/beach_litriochta/netlify_domain_management.png" alt="netlify_domain_management" width="880" height="481"&gt;
&lt;/li&gt;
&lt;li&gt;Add your custom domain(s)&lt;/li&gt;
&lt;li&gt;Click the options dropdown -&amp;gt; Go to DNS panel &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Db9V7JWw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/beach_litriochta/netlify_dns_list.png" alt="netlify_dns_list" width="880" height="479"&gt;
&lt;/li&gt;
&lt;li&gt;Copy the 4 DNS entries from Netlify&lt;/li&gt;
&lt;li&gt;Go to the &lt;a href="https://cp.blacknighthosting.com"&gt;blacknight control panel&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Click the domains dropdown -&amp;gt; your domain &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Sw0qWD-X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/beach_litriochta/blacknight_domain.png" alt="blacknight_domain" width="880" height="480"&gt;
&lt;/li&gt;
&lt;li&gt;Click Nameservers Management &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZHvurTJ1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/beach_litriochta/blacknight_nameservers_management.png" alt="blacknight_nameservers_management" width="880" height="457"&gt;
&lt;/li&gt;
&lt;li&gt;Select custom nameservers and paste in the values you copied from Netlify. &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yumjuJGL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/beach_litriochta/blacknight_custom_nameservers.png" alt="blacknight_custom_nameservers" width="880" height="456"&gt;
&lt;/li&gt;
&lt;li&gt;Now you have to wait “/. I left it overnight, and in the morning my domain was redirecting correctly!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you’re interested in learning Irish or spelling games in general please check out &lt;a href="https://beacha.ie"&gt;https://beacha.ie&lt;/a&gt;! It’s free and the code is open source &lt;a href="https://github.com/ConorSheehan1/beach-litriochta"&gt;https://github.com/ConorSheehan1/beach-litriochta&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>domain</category>
      <category>networking</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Reverse Engineering Uhabits Datamodel</title>
      <dc:creator>Conor Sheehan</dc:creator>
      <pubDate>Mon, 18 Apr 2022 00:00:00 +0000</pubDate>
      <link>https://forem.com/conorsheehan1/reverse-engineering-uhabits-datamodel-2h69</link>
      <guid>https://forem.com/conorsheehan1/reverse-engineering-uhabits-datamodel-2h69</guid>
      <description>&lt;p&gt;OK, that title sounds fancy and complicated. What did I actually do? First I should probably explain why I did anything in the first place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Background
&lt;/h3&gt;

&lt;p&gt;I’ve been using &lt;a href="https://github.com/iSoron/uhabits"&gt;uhabits&lt;/a&gt; for years. I noticed a new &lt;a href="https://github.com/iSoron/uhabits/discussions/42"&gt;numeric habit type&lt;/a&gt; was added in &lt;a href="https://github.com/iSoron/uhabits/releases/tag/v2.0.0-alpha"&gt;v2.0.0&lt;/a&gt;. Previously you could only track habits in a boolean way. Now instead of a checkbox per day, you had a numeric input to track how many times per day you did something.&lt;/p&gt;

&lt;p&gt;I had lots of habits in the old boolean format that would benefit from the new format. E.g. I was tracking my coffee and alcohol consumption using days per week, rather than number of units per week. I looked around, but couldn’t find a way to convert between the habit types. I found &lt;a href="https://github.com/iSoron/uhabits/discussions/934"&gt;this feature request&lt;/a&gt;, but no replies yet. I had a quick look through the codebase, and realized it’d be a lot of work to make a pr since I didn’t know the main language, kotlin. So I decided to try and make my own little utility to convert between the habit types.&lt;/p&gt;

&lt;p&gt;I know uhabits supports importing / exporting it’s internal sqlite database. I don’t know kotlin, but I do now SQL, so instead of trying to figure out the logic from the sourcecode, I created a few habits with the new numeric type, and exported everything. I opened up the exported db and compared the differences between the old and new habits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reverse engineering the datamodel
&lt;/h3&gt;

&lt;p&gt;I could see the tables and fields pretty easily, but that didn’t tell me what they were actually used for. I figured out that the habits and repetitions tables were what I needed to work on, because the others were either empty (events), or metadata tables (android_metadata, sqlite_sequence). &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--O2KA4y9y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/uhabits/tables.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--O2KA4y9y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/uhabits/tables.png" alt="tables" width="880" height="346"&gt;&lt;/a&gt; &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BJjtB3Fj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/uhabits/table_schema.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BJjtB3Fj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/uhabits/table_schema.png" alt="table_schema" width="880" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I needed to start messing with the data to see what would happen. I started with the habits table. &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--U6yAV8cb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/uhabits/habit_data.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--U6yAV8cb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/uhabits/habit_data.png" alt="habit_data" width="880" height="142"&gt;&lt;/a&gt;I noticed &lt;code&gt;type&lt;/code&gt; is 0 for boolean habits and 1 for numeric habits. &lt;code&gt;freq_num&lt;/code&gt; only changes for boolean habits. For numeric habits, it’s always 1. That makes sense since &lt;code&gt;target_value&lt;/code&gt; is generally 0 for boolean habits, but can be any number for numeric habits. &lt;code&gt;freq_den&lt;/code&gt; seems to refer to the time unit. e.g. If I make a boolean habit with a target of 5 times per week I’d get the following for each type:&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;// boolean, aim to do something 5 days every 7 days&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;freq_den&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;freq_num&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target_value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// numeric, aim to do something 5 times every 7 days&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;freq_den&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;freq_num&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="na"&gt;target_value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next I looked at the &lt;code&gt;repetitions&lt;/code&gt; table. I noticed boolean habits generally got a value of 2 when checked, while numeric habits went up in multiples of &lt;code&gt;1000&lt;/code&gt;. It looks like 1000 is used to represent 1.00, so decimals can be added too. e.g. 1.5 would be 1500. &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--u5kwaAkH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/uhabits/repetitions_data.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--u5kwaAkH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/uhabits/repetitions_data.png" alt="repetitions_data" width="880" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once I had identified the differences between habit types in the 2 main tables, I was able to create some python functions to convert from the old boolean type to the new numeric type.&lt;/p&gt;

&lt;p&gt;After converting and re-importing my habits, I noticed that the graphs for boolean and numeric habits didn’t quite match. I decided to add 2 options; 1 to try to preserve the graphs, and another to preserve the data the way uhabits would generate it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a CLI to convert between types
&lt;/h3&gt;

&lt;p&gt;After I was confident my converter worked, I made a little CLI layer to call it from, and published it on github. &lt;a href="https://github.com/ConorSheehan1/uhabits_converter"&gt;https://github.com/ConorSheehan1/uhabits_converter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The end results look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TD1cC_3Y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/uhabits/coffee_bool.jpg" alt="coffee_bool" width="880" height="1760"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--W__mKwdw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://conorsheehan1.github.io/assets/images/uhabits/coffee_num.jpg" alt="coffee_num" width="880" height="1760"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>opensource</category>
      <category>databse</category>
    </item>
    <item>
      <title>Netlify deployments from GitHub without giving write access</title>
      <dc:creator>Conor Sheehan</dc:creator>
      <pubDate>Mon, 21 Feb 2022 17:43:34 +0000</pubDate>
      <link>https://forem.com/conorsheehan1/netlify-deployments-from-github-without-giving-write-access-m3f</link>
      <guid>https://forem.com/conorsheehan1/netlify-deployments-from-github-without-giving-write-access-m3f</guid>
      <description>&lt;p&gt;&lt;a href="https://www.netlify.com/" rel="noopener noreferrer"&gt;Netlify&lt;/a&gt; is a great hosting service, but there's one thing that bothers me about it. The default deployment path involves giving the service &lt;strong&gt;read &lt;em&gt;and&lt;/em&gt; write access&lt;/strong&gt; to all of your &lt;strong&gt;public &lt;em&gt;and&lt;/em&gt; private repositories&lt;/strong&gt; on GitHub 😱&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.netlify.com/site-deploys/create-deploys/#deploy-with-git" rel="noopener noreferrer"&gt;https://docs.netlify.com/site-deploys/create-deploys/#deploy-with-git&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They skip over the authorization step in the video on their docs, but here's what it looks like.&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%2F10y0xw8dha08edpfn6hy.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%2F10y0xw8dha08edpfn6hy.png" alt="Netlify OAuth"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I found &lt;a href="https://answers.netlify.com/t/why-do-you-need-write-permissions-to-my-repository/7897" rel="noopener noreferrer"&gt;some&lt;/a&gt; &lt;a href="https://answers.netlify.com/t/new-github-permissions/37939" rel="noopener noreferrer"&gt;questions&lt;/a&gt; on their forums asking about this. There's even an open &lt;a href="https://github.com/netlify/netlify-cms/issues/4329" rel="noopener noreferrer"&gt;issue&lt;/a&gt; on their GitHub about it. Despite all the links I visited, I didn't find a clear way to automate deploys to Netlify without giving them full access to my GitHub, so here's how I managed to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial drag and drop deploy
&lt;/h2&gt;

&lt;p&gt;Before we can automate our deploys, we need a site ID. Netlify provides a drag and drop feature, so we can drag the output of a build, or even a folder with an empty &lt;code&gt;index.html&lt;/code&gt; to create a new site. &lt;a href="https://app.netlify.com/drop" rel="noopener noreferrer"&gt;https://app.netlify.com/drop&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub action
&lt;/h2&gt;

&lt;p&gt;Now that we have a Netlify site created, we can automate deploys to it. I used &lt;a href="https://github.com/jsmrcaga/action-netlify-deploy" rel="noopener noreferrer"&gt;https://github.com/jsmrcaga/action-netlify-deploy&lt;/a&gt;, which requires an auth token and a site ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate auth token
&lt;/h3&gt;

&lt;p&gt;We can generate a Netlify auth token by going to &lt;a href="https://app.netlify.com/user/applications#personal-access-tokens" rel="noopener noreferrer"&gt;https://app.netlify.com/user/applications#personal-access-tokens&lt;/a&gt;. Click &lt;code&gt;New Access Token&lt;/code&gt;, then give it a description and click &lt;code&gt;generate&lt;/code&gt;. Copy the value, it won't be displayed again. &lt;/p&gt;

&lt;p&gt;To make the value accessible to the GitHub action, go to your GitHub repository and click &lt;code&gt;settings&lt;/code&gt; -&amp;gt; &lt;code&gt;secrets&lt;/code&gt; -&amp;gt; &lt;code&gt;new repository secret&lt;/code&gt;. I named mine &lt;code&gt;NETLIFY_AUTH_TOKEN&lt;/code&gt; and pasted in the value I copied from Netlify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get site ID
&lt;/h3&gt;

&lt;p&gt;You can find your Netlify site ID by going to your Netlify site overview and clicking &lt;code&gt;site settings&lt;/code&gt; and copying the &lt;code&gt;APP ID&lt;/code&gt;. Again, to make it accessible to the GitHub action, click &lt;code&gt;settings&lt;/code&gt; -&amp;gt; &lt;code&gt;secrets&lt;/code&gt; -&amp;gt; &lt;code&gt;new repository secret&lt;/code&gt;. I named mine &lt;code&gt;NETLIFY_SITE_ID&lt;/code&gt; and pasted in the value I copied from Netlify.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub action
&lt;/h3&gt;

&lt;p&gt;Now that we have our secrets set up, we can create our GitHub action. Mine looks something like &lt;a href="https://github.com/ConorSheehan1/conors-cocktails/blob/master/.github/workflows/deploy.yml" rel="noopener noreferrer"&gt;this&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="c1"&gt;# .github/workflows/deploy.yml&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;deploy&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# enable manual deploys&lt;/span&gt;
  &lt;span class="c1"&gt;# https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# deploy tags and commits to master automatically&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&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;*"&lt;/span&gt;
    &lt;span class="na"&gt;branches&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;master"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deploy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Netlify"&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jsmrcaga/action-netlify-deploy@v1.7.2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# pass secrets in to the action&lt;/span&gt;
          &lt;span class="na"&gt;NETLIFY_SITE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NETLIFY_SITE_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;NETLIFY_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NETLIFY_AUTH_TOKEN }}&lt;/span&gt;
          &lt;span class="c1"&gt;# add the GitHub ref to the deploy message so we can trace back what version is deployed from the Netlify side&lt;/span&gt;
          &lt;span class="na"&gt;NETLIFY_DEPLOY_MESSAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prod&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deploy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;v${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;github.ref&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
          &lt;span class="na"&gt;NETLIFY_DEPLOY_TO_PROD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="c1"&gt;# this bit should be custom to your project. I'm deploying a vuepress project that uses yarn, so these are my settings.&lt;/span&gt;
          &lt;span class="na"&gt;install_command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install&lt;/span&gt;
          &lt;span class="na"&gt;build_command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn build&lt;/span&gt;
          &lt;span class="na"&gt;build_directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/.vuepress/dist&lt;/span&gt;
          &lt;span class="na"&gt;node_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;14.18.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! Now you can deploy to Netlify automatically from your GitHub repo, and you haven't given up any access rights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example site
&lt;/h2&gt;

&lt;p&gt;I figured this out while building this project &lt;a href="https://github.com/ConorSheehan1/conors-cocktails" rel="noopener noreferrer"&gt;https://github.com/ConorSheehan1/conors-cocktails&lt;/a&gt; which is deployed here &lt;a href="https://conorscocktails.netlify.app" rel="noopener noreferrer"&gt;https://conorscocktails.netlify.app&lt;/a&gt; if you're interested.&lt;/p&gt;

</description>
      <category>netlify</category>
      <category>github</category>
      <category>devops</category>
    </item>
    <item>
      <title>Add BootstrapVue to VuePress</title>
      <dc:creator>Conor Sheehan</dc:creator>
      <pubDate>Tue, 25 Jan 2022 17:51:12 +0000</pubDate>
      <link>https://forem.com/conorsheehan1/add-bootstrapvue-to-vuepress-3h7g</link>
      <guid>https://forem.com/conorsheehan1/add-bootstrapvue-to-vuepress-3h7g</guid>
      <description>&lt;h2&gt;
  
  
  Create a VuePress project
&lt;/h2&gt;

&lt;p&gt;The first thing we need to do is create a new &lt;a href="https://vuepress.vuejs.org/"&gt;VuePress&lt;/a&gt; project. It's really easy with the latest version of npm or yarn. From the &lt;a href="https://vuepress.vuejs.org/guide/getting-started.html#quick-start"&gt;VuePress docs&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn create vuepress-site &lt;span class="nv"&gt;$optionalDirectoryName&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install BootstrapVue
&lt;/h2&gt;

&lt;p&gt;Next we install &lt;a href="https://bootstrap-vue.org"&gt;BootstrapVue&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add bootstrap bootstrap-vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Now we need to import BootstrapVue in &lt;code&gt;.vuepress/enhanceApp.js&lt;/code&gt;, where we have access to the vue instance.&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;// .vuepress/enhanceApp.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BootstrapVue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IconsPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bootstrap-vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;({&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;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;siteData&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="c1"&gt;// Make BootstrapVue available throughout your project&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;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BootstrapVue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Optionally install the BootstrapVue icon components plugin&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;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;IconsPlugin&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;Finally we need to load the bootstrap css. VuePress ships with &lt;a href="https://stylus-lang.com/"&gt;stylus&lt;/a&gt; by default now, but we can still import css into our stylus file at &lt;code&gt;.vuepress/styles/index.styl&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;/**
 * Custom Styles here.
 *
 * ref：https://v1.vuepress.vuejs.org/config/#index-styl
 */

@require '~bootstrap/dist/css/bootstrap.css'
@require '~bootstrap-vue/dist/bootstrap-vue.css'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! Now you can use BootstrapVue components in your VuePress app. &lt;/p&gt;

&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;Vuepress lets you embed components directly in markdown, so you can do something like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- src/index.md --&amp;gt;&lt;/span&gt;
&lt;span class="gu"&gt;## Hi from bootstrap-vue&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;b-button&amp;gt;&lt;/span&gt;Hello world!&lt;span class="nt"&gt;&amp;lt;/b-button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's an example app I've deployed to netlify which uses various BootstrapVue components including &lt;code&gt;b-carousel&lt;/code&gt; and &lt;code&gt;b-table&lt;/code&gt;: &lt;a href="https://conorscocktails.netlify.app/"&gt;https://conorscocktails.netlify.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find the sourcecode here: &lt;a href="https://github.com/ConorSheehan1/conors-cocktails"&gt;https://github.com/ConorSheehan1/conors-cocktails&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vue</category>
      <category>webdev</category>
      <category>vuepress</category>
      <category>javascript</category>
    </item>
    <item>
      <title>3 Patterns for Cookiecutter Templates</title>
      <dc:creator>Conor Sheehan</dc:creator>
      <pubDate>Tue, 27 Jul 2021 17:03:20 +0000</pubDate>
      <link>https://forem.com/conorsheehan1/3-patterns-for-cookiecutter-templates-aha</link>
      <guid>https://forem.com/conorsheehan1/3-patterns-for-cookiecutter-templates-aha</guid>
      <description>&lt;h2&gt;
  
  
  TOC
&lt;/h2&gt;

&lt;p&gt;Intro&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hooks&lt;/li&gt;
&lt;li&gt;
Tests

&lt;ol&gt;
&lt;li&gt;Inside the template&lt;/li&gt;
&lt;li&gt;Outside the template&lt;/li&gt;
&lt;li&gt;CI&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;li&gt;Install from GitHub&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Intro
&lt;/h3&gt;

&lt;p&gt;If you've heard of &lt;a href="https://github.com/cookiecutter/cookiecutter"&gt;cookiecutter&lt;/a&gt; you can skip this part.&lt;/p&gt;

&lt;p&gt;Cookiecutter is a command-line utility that creates projects from templates. There's a list of &lt;a href="https://github.com/cookiecutter/cookiecutter#cookiecutter-specials"&gt;templates maintained by the cookiecutter team&lt;/a&gt; and plenty of  &lt;a href="https://awesomeopensource.com/projects/cookiecutter"&gt;community awesome lists&lt;/a&gt;. It's built with &lt;a href="https://www.python.org/"&gt;python&lt;/a&gt; and uses the &lt;a href="https://github.com/pallets/jinja"&gt;jinja&lt;/a&gt; templating framework (found in python web frameworks like &lt;a href="https://flask.palletsprojects.com"&gt;flask&lt;/a&gt;). You can use it to make a template for pretty much anything! All you need to get started is &lt;code&gt;pip install cookiecutter&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks
&lt;/h2&gt;

&lt;p&gt;Cookiecutter provides &lt;a href="https://cookiecutter.readthedocs.io/en/1.7.3/advanced/hooks.html"&gt;pre and post generate scripts&lt;/a&gt;. They are Python or Shell scripts that run before and/or after your project is generated.&lt;/p&gt;

&lt;p&gt;They can be really useful. For example, if you want to get the absolute path to the generated project, you can use a post generate script to replace a specific piece of text with the absolute path. e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cookiecutter-$your-project/hooks/post_gen_project.py 
&lt;/span&gt;&lt;span class="n"&gt;abs_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getcwd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dirs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;abs_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'replace_me.base_dir'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abs_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;'w'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's an &lt;a href="https://github.com/ConorSheehan1/cookiecutter-jira-project/blob/master/hooks/post_gen_project.py#L3"&gt;example in a cookiecutter I made&lt;/a&gt;. &lt;br&gt;
See &lt;a href="https://github.com/cookiecutter/cookiecutter/issues/955#issuecomment-444864537"&gt;https://github.com/cookiecutter/cookiecutter/issues/955#issuecomment-444864537&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Tests
&lt;/h2&gt;

&lt;p&gt;There are a few ways to test cookiecutters.&lt;/p&gt;
&lt;h3&gt;
  
  
  Putting tests inside the template
&lt;/h3&gt;

&lt;p&gt;This approach has the advantage that when someone generates a project using your template, they already have tests set up. e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# {{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}.py
&lt;/span&gt;&lt;span class="n"&gt;__version__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="err"&gt;{{&lt;/span&gt;&lt;span class="nf"&gt;cookiecutter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;}}(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&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;version&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;__version__&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# do some cli stuff
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# {{cookiecutter.repo_name}}/tests/test_{{cookiecutter.repo_name}}.py 
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;unittest&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;cookiecutter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;}}(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;cookiecutter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;}}(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's an &lt;a href="https://github.com/ConorSheehan1/cookiecutter-fire-cli/blob/621b635c23407b9704bcce322390dbebbc544ca3/%7B%7Bcookiecutter.repo_name%7D%7D/tests/test_%7B%7Bcookiecutter.repo_name%7D%7D.py#L1"&gt;example in a cookiecutter I made&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting tests outside the template
&lt;/h3&gt;

&lt;p&gt;This approach is useful if it doesn't make sense to include tests in the generated project, but you still want to test what is generated. Note: this doesn't mean trying to test cookiecutter itself!&lt;/p&gt;

&lt;p&gt;Normally Cookiecutter opens a prompt to get user input to be injected into your template. You can bypass this with the &lt;code&gt;--no-input&lt;/code&gt; argument. It also allows you to &lt;a href="https://github.com/cookiecutter/cookiecutter/pull/666"&gt;pass values required by &lt;code&gt;cookiecutter.json&lt;/code&gt; as arguments&lt;/a&gt;. e.g.&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cookiecutter-$your-project/cookiecutter.json&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;"project_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;"alphabet"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# this will generate a project named foo instead of alphabet&lt;/span&gt;
cookiecutter &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--no-input&lt;/span&gt; &lt;span class="nv"&gt;project_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"foo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've used this approach when creating cookiecutters that contain scripts rather than full projects. To test the scripts I generate a project, import and run functions from the scripts, and test the output. e.g.&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;# cookiecutter-$your-project/{{cookiecutter.project_name|lower}}/script.sh&lt;/span&gt;
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="o"&gt;{{&lt;/span&gt;cookiecutter.project_name|lower&lt;span class="o"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt;_repo_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{{cookiecutter.repo_dir}}"&lt;/span&gt;

goto_&lt;span class="o"&gt;{{&lt;/span&gt;cookiecutter.project_name|lower&lt;span class="o"&gt;}}&lt;/span&gt;_repo&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;{cookiecutter.project_name|lower&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;}_repo_dir"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# cookiecutter-$your-project/tests/test_helper.bash&lt;/span&gt;
setup&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# we expect foo/script.sh to be generated&lt;/span&gt;
    load &lt;span class="s2"&gt;"foo/script.sh"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# cookiecutter-$your-project/tests/script.bats&lt;/span&gt;
&lt;span class="c"&gt;#!/usr/bin/env bats&lt;/span&gt;

load &lt;span class="s2"&gt;"test_helper"&lt;/span&gt;

&lt;span class="c"&gt;# we expect a function named goto_foo_repo in foo/script.sh&lt;/span&gt;
@test &lt;span class="s2"&gt;"goto_foo_repo"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  goto_foo_repo
  assert_equal &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$foo_repo_dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's an &lt;a href="https://github.com/ConorSheehan1/cookiecutter-jira-project/blob/60e341060198a4d8937095b6c2e53f545d1ff58f/tests/utils.bats#L8"&gt;example in a cookiecutter I made&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI
&lt;/h3&gt;

&lt;p&gt;Now that you have tests set up, you can set up continuous integration! The important bit of here is&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="s"&gt;cookiecutter&lt;/span&gt; 
&lt;span class="s"&gt;.&lt;/span&gt; &lt;span class="c1"&gt;# create a project using the current directory as a template&lt;/span&gt;
&lt;span class="s"&gt;--overwrite-if-exists&lt;/span&gt; &lt;span class="c1"&gt;# if the destination directory exists overwrite it&lt;/span&gt;
&lt;span class="s"&gt;--no-input&lt;/span&gt; &lt;span class="c1"&gt;# don't prompt for user input. &lt;/span&gt;
&lt;span class="c1"&gt;# since there are no other args, use default values from cookiecutter.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's an example with github actions&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="c1"&gt;# .github/workflows/ci.yml&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;ci&lt;/span&gt;

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

    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;python&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;3.6&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;3.7&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;3.8&lt;/span&gt;&lt;span class="pi"&gt;]&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&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;Set up Python ${{ matrix.python }}&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.python }}&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;Install Poetry&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;python -m pip install --upgrade pip&lt;/span&gt;
          &lt;span class="s"&gt;pip install poetry&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;Install python packages&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;poetry install&lt;/span&gt;
      &lt;span class="c1"&gt;# here's the important bit!&lt;/span&gt;
      &lt;span class="c1"&gt;# generate a new project using the cookiecutter template&lt;/span&gt;
      &lt;span class="c1"&gt;# use the default values in cookiecutter.json with --no-input&lt;/span&gt;
      &lt;span class="c1"&gt;# if the directory already exists, overwrite it&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;Generate package using cookiecutter&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;poetry run cookiecutter . --overwrite-if-exists --no-input&lt;/span&gt;
      &lt;span class="c1"&gt;# now inside the generated project, install dependencies and run tests&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;Install python packages (in cookiecutter dir)&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example_cli&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;poetry install&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;Run tests (in cookiecutter dir)&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example_cli&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;poetry run task tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's an &lt;a href="https://github.com/ConorSheehan1/cookiecutter-fire-cli/blob/621b635c23407b9704bcce322390dbebbc544ca3/.github/workflows/ci.yml#L1"&gt;example in a cookiecutter I made&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install from GitHub
&lt;/h2&gt;

&lt;p&gt;Cookiecutter provides a &lt;a href="https://cookiecutter.readthedocs.io/en/1.7.3/usage.html#works-directly-with-git-and-hg-mercurial-repos-too"&gt;really easy way to use templates hosted on github&lt;/a&gt;. All you need is &lt;code&gt;cookiecutter gh:$username/$repo&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Hopefully now you should be able to create a Cookiecutter template with hooks, tests, and CI, all easily installable from GitHub!&lt;/p&gt;

</description>
      <category>cookiecutter</category>
      <category>template</category>
      <category>python</category>
      <category>firstpost</category>
    </item>
  </channel>
</rss>
