<?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>DEV Community: Isaac Lee</title>
    <description>The latest articles on DEV Community by Isaac Lee (@ijlee2).</description>
    <link>https://dev.to/ijlee2</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%2F86322%2Fbfa4a0cf-d246-4df6-8c36-3c45f6093a50.jpeg</url>
      <title>DEV Community: Isaac Lee</title>
      <link>https://dev.to/ijlee2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ijlee2"/>
    <language>en</language>
    <item>
      <title>Migrating to &lt;template&gt; tag</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Mon, 13 Oct 2025 12:20:47 +0000</pubDate>
      <link>https://dev.to/ijlee2/migrating-to-tag-39on</link>
      <guid>https://dev.to/ijlee2/migrating-to-tag-39on</guid>
      <description>&lt;p&gt;&lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; tag lets us write template(s) in &lt;a href="https://rfcs.emberjs.com/id/0496-handlebars-strict-mode/" rel="noopener noreferrer"&gt;strict mode&lt;/a&gt;. Templates are more explicit and statically analyzable, so we can adopt more tools from the JavaScript ecosystem. There's less work for Embroider to do, so we can expect app builds to be faster.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; tag has been around for a few years now. With the help of addons, &lt;a href="https://github.com/ember-cli/ember-template-imports" rel="noopener noreferrer"&gt;in components and tests&lt;/a&gt; since 2020 and &lt;a href="https://github.com/discourse/ember-route-template" rel="noopener noreferrer"&gt;in routes&lt;/a&gt; since 2023. In short, it's stable and you can likely use &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; tag in your projects today.&lt;/p&gt;

&lt;p&gt;This post aims to accelerate adoption. I will show you step-by-step how to migrate an existing component, route, and test. Once you understand the idea, feel free to &lt;a href="https://github.com/ijlee2/ember-codemod-add-template-tags" rel="noopener noreferrer"&gt;run my codemod&lt;/a&gt; to automate the steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Where did things come from?
&lt;/h2&gt;

&lt;p&gt;To enter strict mode, we will be importing objects (e.g. components, helpers, modifiers) instead of letting string names in templates be somehow resolved. So let's first look at where things come from.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Objects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@ember/component&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Input, Textarea&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@ember/helper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;array, concat, fn, get, hash, uniqueId&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@ember/modifier&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@ember/routing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;LinkTo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@ember/template&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;htmlSafe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@embroider/util&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ensureSafeComponent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The right column shows objects native to Ember. We name-import them from the path shown in the left. For example,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;array&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="s1"&gt;@ember/helper&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;Notice, there's a naming convention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Component names follow Pascal case. (Capitalize the first letter of each word.)&lt;/li&gt;
&lt;li&gt;All other names follow camel case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Ember addons, you can always do a default-import. However, for each object, you have to remember the full path, add 1 line of code, and type many characters. It's also easy to introduce inconsistencies in the import name (the "local" name).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ContainerQuery&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;ember-container-query/components/container-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;height&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;ember-container-query/helpers/height&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;width&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;ember-container-query/helpers/width&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;Due to these reasons, always do name-imports when an addon provides a barrel file. If it doesn't, consider making a pull request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ContainerQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&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="s1"&gt;ember-container-query&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;h2&gt;
  
  
  2. How to migrate
&lt;/h2&gt;

&lt;p&gt;We will consider a component just complex enough to cover the important steps. You can use &lt;a href="https://github.com/ijlee2/ember-workshop" rel="noopener noreferrer"&gt;ember-workshop&lt;/a&gt; to test the code shown below. &lt;code&gt;&amp;lt;Hello&amp;gt;&lt;/code&gt; is defined in &lt;code&gt;my-addon&lt;/code&gt;, then rendered and tested in &lt;code&gt;my-app&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  a. Components
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;Hello&amp;gt;&lt;/code&gt; is a Glimmer component that receives 1 argument. For simplicity, we will ignore the component signature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* my-addon: src/components/hello.module.css */&lt;/span&gt;
&lt;span class="nc"&gt;.container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;orange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;italic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hide&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fade-out&lt;/span&gt; &lt;span class="m"&gt;0s&lt;/span&gt; &lt;span class="n"&gt;ease-in&lt;/span&gt; &lt;span class="m"&gt;0s&lt;/span&gt; &lt;span class="n"&gt;forwards&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hide.after-3-sec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;fade-out&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{! my-addon: src/components/hello.hbs }}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;
    &lt;span class="nv"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;styles&lt;/span&gt;
    &lt;span class="s2"&gt;"container"&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;someCondition&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;array&lt;/span&gt; &lt;span class="s2"&gt;"hide"&lt;/span&gt; &lt;span class="s2"&gt;"after-3-sec"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;t&lt;/span&gt; &lt;span class="s2"&gt;"hello.message"&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="na"&gt;@name&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;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 typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* my-addon: src/components/hello.ts */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Component&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;@glimmer/component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&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;./hello.module.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;HelloSignature&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;Args&lt;/span&gt;&lt;span class="p"&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="kr"&gt;string&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Hello&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HelloSignature&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;someCondition&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1. Change the class' file extension to &lt;code&gt;.gts&lt;/code&gt;. Insert an empty &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; tag at the end of the class body.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* my-addon: src/components/hello.gts */
&lt;span class="p"&gt;import Component from '@glimmer/component';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;import styles from './hello.module.css';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export default class Hello extends Component&amp;lt;HelloSignature&amp;gt; {
&lt;/span&gt;  styles = styles;
&lt;span class="err"&gt;
&lt;/span&gt;  get someCondition(): boolean {
    return true;
  }
&lt;span class="gi"&gt;+
+   &amp;lt;template&amp;gt;
+   &amp;lt;/template&amp;gt;
&lt;/span&gt;}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2. Copy the template from &lt;code&gt;.hbs&lt;/code&gt; and paste it into the &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; tag. Delete the &lt;code&gt;.hbs&lt;/code&gt; file afterwards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* my-addon: src/components/hello.gts */
&lt;span class="p"&gt;import Component from '@glimmer/component';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;import styles from './hello.module.css';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export default class Hello extends Component&amp;lt;HelloSignature&amp;gt; {
&lt;/span&gt;  styles = styles;
&lt;span class="err"&gt;
&lt;/span&gt;  get someCondition(): boolean {
    return true;
  }
&lt;span class="err"&gt;
&lt;/span&gt;  &amp;lt;template&amp;gt;
&lt;span class="gi"&gt;+     &amp;lt;div
+       class={{local
+         this.styles
+         "container"
+         (if this.someCondition (array "hide" "after-3-sec"))
+       }}
+     &amp;gt;
+       {{t "hello.message" name=@name}}
+     &amp;lt;/div&amp;gt;
&lt;/span&gt;  &amp;lt;/template&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3. Enter strict mode. That is, specify where things come from.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* my-addon: src/components/hello.gts */
&lt;span class="gi"&gt;+ import { array } from '@ember/helper';
&lt;/span&gt;&lt;span class="p"&gt;import Component from '@glimmer/component';
&lt;/span&gt;&lt;span class="gi"&gt;+ import { t } from 'ember-intl';
+ import { local } from 'embroider-css-modules';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;import styles from './hello.module.css';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export default class Hello extends Component&amp;lt;HelloSignature&amp;gt; {
&lt;/span&gt;  styles = styles;
&lt;span class="err"&gt;
&lt;/span&gt;  get someCondition(): boolean {
    return true;
  }
&lt;span class="err"&gt;
&lt;/span&gt;  &amp;lt;template&amp;gt;
    &amp;lt;div
      class={{local
        this.styles
        "container"
        (if this.someCondition (array "hide" "after-3-sec"))
      }}
    &amp;gt;
      {{t "hello.message" name=@name}}
    &amp;lt;/div&amp;gt;
  &amp;lt;/template&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4. (Optional) Remove unnecessary code. Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Constants passed to the template via the class&lt;/li&gt;
&lt;li&gt;Template registry (the &lt;code&gt;declare module&lt;/code&gt; block), if you no longer have to support &lt;code&gt;*.hbs&lt;/code&gt; files or &lt;code&gt;hbs&lt;/code&gt; tags.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ensureSafeComponent()&lt;/code&gt; from &lt;code&gt;@embroider/util&lt;/code&gt; (pass the component directly)&lt;/li&gt;
&lt;li&gt;One-off helpers
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* my-addon: src/components/hello.gts */
&lt;span class="p"&gt;import { array } from '@ember/helper';
import Component from '@glimmer/component';
import { t } from 'ember-intl';
import { local } from 'embroider-css-modules';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;import styles from './hello.module.css';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export default class Hello extends Component&amp;lt;HelloSignature&amp;gt; {
&lt;/span&gt;&lt;span class="gd"&gt;-   styles = styles;
-
&lt;/span&gt;  get someCondition(): boolean {
    return true;
  }
&lt;span class="err"&gt;
&lt;/span&gt;  &amp;lt;template&amp;gt;
    &amp;lt;div
      class={{local
&lt;span class="gd"&gt;-         this.styles
&lt;/span&gt;&lt;span class="gi"&gt;+         styles
&lt;/span&gt;        "container"
        (if this.someCondition (array "hide" "after-3-sec"))
      }}
    &amp;gt;
      {{t "hello.message" name=@name}}
    &amp;lt;/div&amp;gt;
  &amp;lt;/template&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note, we pass the signature of a Glimmer component to the base class &lt;code&gt;Component&lt;/code&gt;. For template-only components, we can provide the signature in two ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* my-addon: src/components/hello.gts */
import type { TOC } from '@ember/component/template-only';
import { t } from 'ember-intl';

import styles from './hello.module.css';

const Hello: TOC&amp;lt;HelloSignature&amp;gt; = &amp;lt;template&amp;gt;
  &amp;lt;div class={{styles.container}}&amp;gt;
    {{t "hello.message" name=@name}}
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;;

export default Hello;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* my-addon: src/components/hello.gts */
import type { TOC } from '@ember/component/template-only';
import { t } from 'ember-intl';

import styles from './hello.module.css';

&amp;lt;template&amp;gt;
  &amp;lt;div class={{styles.container}}&amp;gt;
    {{t "hello.message" name=@name}}
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt; satisfies TOC&amp;lt;HelloSignature&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first variation (assigns the template to a variable, uses type annotation) may help with debugging and searching code, as the variable gives a name to the component. The second (no assignment, use of &lt;code&gt;satisfies&lt;/code&gt;) is what Ember CLI currently uses in its blueprints.&lt;/p&gt;

&lt;h3&gt;
  
  
  b. Routes
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;my-app&lt;/code&gt;, the &lt;code&gt;index&lt;/code&gt; route renders &lt;code&gt;&amp;lt;Hello&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{! my-app: app/templates/index.hbs }}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;container&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Hello&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;userName&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="err"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="na"&gt;div&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We see that the controller provides styles and &lt;code&gt;userName&lt;/code&gt;, a getter that returns some string.&lt;/p&gt;

&lt;p&gt;1. Change the file extension to &lt;code&gt;.gts&lt;/code&gt;. Surround the template with the &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; tag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* my-app: app/templates/index.gts */
&lt;span class="gi"&gt;+ &amp;lt;template&amp;gt;
&lt;/span&gt;  &amp;lt;div class={{this.styles.container}}&amp;gt;
    &amp;lt;Hello @name={{this.userName}}
  &amp;lt;/div&amp;gt;
&lt;span class="gi"&gt;+ &amp;lt;/template&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2. Enter strict mode. Instead of &lt;code&gt;this&lt;/code&gt;, write &lt;code&gt;@controller&lt;/code&gt; to indicate things from the controller. Just like before, &lt;code&gt;@model&lt;/code&gt; refers to the &lt;code&gt;model&lt;/code&gt; hook's return value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* my-app: app/templates/index.gts */
&lt;span class="gi"&gt;+ import { Hello } from 'my-addon';
+
&lt;/span&gt;&amp;lt;template&amp;gt;
&lt;span class="gd"&gt;-   &amp;lt;div class={{this.styles.container}}&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+   &amp;lt;div class={{@controller.styles.container}}&amp;gt;
&lt;/span&gt;&lt;span class="gd"&gt;-     &amp;lt;Hello @name={{this.userName}}
&lt;/span&gt;&lt;span class="gi"&gt;+     &amp;lt;Hello @name={{@controller.userName}}
&lt;/span&gt;  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3. The prior code is enough for &lt;code&gt;.gjs&lt;/code&gt;, but not for &lt;code&gt;.gts&lt;/code&gt;. If the template includes &lt;code&gt;@controller&lt;/code&gt; or &lt;code&gt;@model&lt;/code&gt;, provide the signature (their types).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* my-app: app/templates/index.gts */
&lt;span class="gi"&gt;+ import type { TOC } from '@ember/component/template-only';
&lt;/span&gt;&lt;span class="p"&gt;import { Hello } from 'my-addon';
&lt;/span&gt;&lt;span class="gi"&gt;+ import type IndexController from 'my-app/controllers/index';
+
+ interface IndexSignature {
+   Args: {
+     controller: IndexController;
+     model: unknown;
+   };
+ }
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class={{@controller.styles.container}}&amp;gt;
    &amp;lt;Hello @name={{@controller.userName}}
  &amp;lt;/div&amp;gt;
&lt;span class="gd"&gt;- &amp;lt;/template&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ &amp;lt;/template&amp;gt; satisfies TOC&amp;lt;IndexSignature&amp;gt;;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4. (Optional) Remove unnecessary code. Here, we can colocate the stylesheet so that we rely less on the controller (with the aim of removing it).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* my-app: app/templates/index.gts */
&lt;span class="p"&gt;import type { TOC } from '@ember/component/template-only';
import { Hello } from 'my-addon';
import type IndexController from 'my-app/controllers/index';
&lt;/span&gt;&lt;span class="gi"&gt;+
+ import styles from './index.module.css';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface IndexSignature {
&lt;/span&gt;  Args: {
    controller: IndexController;
    model: unknown;
  };
}
&lt;span class="err"&gt;
&lt;/span&gt;&amp;lt;template&amp;gt;
&lt;span class="gd"&gt;-   &amp;lt;div class={{@controller.styles.container}}&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+   &amp;lt;div class={{styles.container}}&amp;gt;
&lt;/span&gt;    &amp;lt;Hello @name={{@controller.userName}}
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt; satisfies TOC&amp;lt;IndexSignature&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can even use a Glimmer component to remove states from the controller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* my-app: app/templates/index.gts */
import Component from '@glimmer/component';
import { Hello } from 'my-addon';

import styles from './index.module.css';

interface IndexSignature {
  Args: {
    controller: unknown;
    model: unknown;
  };
}

export default class IndexRoute extends Component&amp;lt;IndexSignature&amp;gt; {
  get userName(): string {
    return 'Zoey';
  }

  &amp;lt;template&amp;gt;
    &amp;lt;div class={{styles.container}}&amp;gt;
      &amp;lt;Hello @name={{this.userName}}
    &amp;lt;/div&amp;gt;
  &amp;lt;/template&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  c. Tests
&lt;/h3&gt;

&lt;p&gt;Last but not least, let's update the test file for &lt;code&gt;&amp;lt;Hello&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* my-app: tests/integration/components/hello-test.ts */&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;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TestContext&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;BaseTestContext&lt;/span&gt;&lt;span class="p"&gt;,&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="s1"&gt;@ember/test-helpers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;hbs&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="s1"&gt;ember-cli-htmlbars&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;setupRenderingTest&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="s1"&gt;my-app/tests/helpers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;test&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="s1"&gt;qunit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TestContext&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;BaseTestContext&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Integration | Component | hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setupRenderingTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Zoey&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;it renders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TestContext&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;hbs&lt;/span&gt;&lt;span class="s2"&gt;`
        &amp;lt;Hello @name={{this.userName}} /&amp;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;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;hasText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello, Zoey!&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1. Change the file extension, replace the &lt;code&gt;hbs&lt;/code&gt; tags with &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;, then enter strict mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* my-app: tests/integration/components/hello-test.gts */
&lt;span class="p"&gt;import {
&lt;/span&gt;  render,
  type TestContext as BaseTestContext,
} from '@ember/test-helpers';
&lt;span class="gd"&gt;- import { hbs } from 'ember-cli-htmlbars';
&lt;/span&gt;&lt;span class="gi"&gt;+ import { Hello } from 'my-addon';
&lt;/span&gt;&lt;span class="p"&gt;import { setupRenderingTest } from 'my-app/tests/helpers';
import { module, test } from 'qunit';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface TestContext extends BaseTestContext {
&lt;/span&gt;  userName: string;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;module('Integration | Component | hello', function (hooks) {
&lt;/span&gt;  setupRenderingTest(hooks);
&lt;span class="err"&gt;
&lt;/span&gt;  hooks.beforeEach(function (this: TestContext) {
    this.userName = 'Zoey';
  });
&lt;span class="err"&gt;
&lt;/span&gt;  test('it renders', async function (this: TestContext, assert) {
&lt;span class="gd"&gt;-     await render&amp;lt;TestContext&amp;gt;(
&lt;/span&gt;&lt;span class="gi"&gt;+     await render(
&lt;/span&gt;&lt;span class="gd"&gt;-       hbs`
&lt;/span&gt;&lt;span class="gi"&gt;+       &amp;lt;template&amp;gt;
&lt;/span&gt;        &amp;lt;Hello @name={{this.userName}} /&amp;gt;
&lt;span class="gd"&gt;-       `,
&lt;/span&gt;&lt;span class="gi"&gt;+       &amp;lt;/template&amp;gt;,
&lt;/span&gt;    );
&lt;span class="err"&gt;
&lt;/span&gt;    assert.dom().hasText('Hello, Zoey!');
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template inside &lt;code&gt;render()&lt;/code&gt; is in strict mode, so &lt;code&gt;glint&lt;/code&gt; no longer needs an extra &lt;code&gt;TestContext&lt;/code&gt; to analyze &lt;code&gt;render()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;2. You may have used QUnit's &lt;code&gt;beforeEach&lt;/code&gt; hook to define things that will be passed to the template (for all tests of a module). Currently, a bug in Ember causes assertions to fail if we keep using &lt;code&gt;this&lt;/code&gt; to pass things to the template.&lt;/p&gt;

&lt;p&gt;We can get around this issue (pun intended) in a few different ways. One is to create an alias of &lt;code&gt;this&lt;/code&gt;, called &lt;code&gt;self&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* my-app: tests/integration/components/hello-test.gts */
&lt;span class="p"&gt;import {
&lt;/span&gt;  render,
  type TestContext as BaseTestContext,
} from '@ember/test-helpers';
&lt;span class="p"&gt;import { Hello } from 'my-addon';
import { setupRenderingTest } from 'my-app/tests/helpers';
import { module, test } from 'qunit';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface TestContext extends BaseTestContext {
&lt;/span&gt;  userName: string;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;module('Integration | Component | hello', function (hooks) {
&lt;/span&gt;  setupRenderingTest(hooks);
&lt;span class="err"&gt;
&lt;/span&gt;  hooks.beforeEach(function (this: TestContext) {
    this.userName = 'Zoey';
  });
&lt;span class="err"&gt;
&lt;/span&gt;  test('it renders', async function (this: TestContext, assert) {
&lt;span class="gi"&gt;+     const self = this;
+
&lt;/span&gt;    await render(
      &amp;lt;template&amp;gt;
&lt;span class="gd"&gt;-         &amp;lt;Hello @name={{this.userName}} /&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+         &amp;lt;Hello @name={{self.userName}} /&amp;gt;
&lt;/span&gt;      &amp;lt;/template&amp;gt;,
    );
&lt;span class="err"&gt;
&lt;/span&gt;    assert.dom().hasText('Hello, Zoey!');
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another is to destructure &lt;code&gt;this&lt;/code&gt; so that we can pass things as variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* my-app: tests/integration/components/hello-test.gts */
&lt;span class="p"&gt;import {
&lt;/span&gt;  render,
  type TestContext as BaseTestContext,
} from '@ember/test-helpers';
&lt;span class="p"&gt;import { Hello } from 'my-addon';
import { setupRenderingTest } from 'my-app/tests/helpers';
import { module, test } from 'qunit';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;interface TestContext extends BaseTestContext {
&lt;/span&gt;  userName: string;
}
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;module('Integration | Component | hello', function (hooks) {
&lt;/span&gt;  setupRenderingTest(hooks);
&lt;span class="err"&gt;
&lt;/span&gt;  hooks.beforeEach(function (this: TestContext) {
    this.userName = 'Zoey';
  });
&lt;span class="err"&gt;
&lt;/span&gt;  test('it renders', async function (this: TestContext, assert) {
&lt;span class="gi"&gt;+     const { userName } = this;
+
&lt;/span&gt;    await render(
      &amp;lt;template&amp;gt;
&lt;span class="gd"&gt;-         &amp;lt;Hello @name={{this.userName}} /&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+         &amp;lt;Hello @name={{userName}} /&amp;gt;
&lt;/span&gt;      &amp;lt;/template&amp;gt;,
    );
&lt;span class="err"&gt;
&lt;/span&gt;    assert.dom().hasText('Hello, Zoey!');
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A third option is to define variables at a global level (scoped to a test module). By doing so, we may be able to remove &lt;code&gt;TestContext&lt;/code&gt; altogether.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* my-app: tests/integration/components/hello-test.gts */
import { render } from '@ember/test-helpers';
import { Hello } from 'my-addon';
import { setupRenderingTest } from 'my-app/tests/helpers';
import { module, test } from 'qunit';

module('Integration | Component | hello', function (hooks) {
  setupRenderingTest(hooks);

  const userName = 'Zoey';

  test('it renders', async function (assert) {
    await render(
      &amp;lt;template&amp;gt;
        &amp;lt;Hello @name={{userName}} /&amp;gt;
      &amp;lt;/template&amp;gt;,
    );

    assert.dom().hasText('Hello, Zoey!');
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, to prevent misusing global variables, to customize test setups, or to facilitate the removal of dead code, you can define things locally instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* my-app: tests/integration/components/hello-test.gts */
import { render } from '@ember/test-helpers';
import { Hello } from 'my-addon';
import { setupRenderingTest } from 'my-app/tests/helpers';
import { module, test } from 'qunit';

module('Integration | Component | hello', function (hooks) {
  setupRenderingTest(hooks);

  test('it renders', async function (assert) {
    const userName = 'Zoey';

    await render(
      &amp;lt;template&amp;gt;
        &amp;lt;Hello @name={{userName}} /&amp;gt;
      &amp;lt;/template&amp;gt;,
    );

    assert.dom().hasText('Hello, Zoey!');
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. ember-codemod-add-template-tags
&lt;/h2&gt;

&lt;p&gt;Time to automate. In &lt;a href="https://crunchingnumbers.live/2025/09/22/large-scale-migrations-with-codemods/" rel="noopener noreferrer"&gt;an earlier post&lt;/a&gt;, I revealed a codemod that performs static code analysis and supports apps, addons, and monorepos.&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;# From the workspace or package root&lt;/span&gt;
pnpx ember-codemod-add-template-tags
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In case you'd like to incrementally migrate, the codemod also provides the options &lt;code&gt;--convert&lt;/code&gt; and &lt;code&gt;--folder&lt;/code&gt;.&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;# Components and tests only&lt;/span&gt;
pnpx ember-codemod-add-template-tags &lt;span class="nt"&gt;--convert&lt;/span&gt; components tests

&lt;span class="c"&gt;# `ui/form` folder only&lt;/span&gt;
pnpx ember-codemod-add-template-tags &lt;span class="nt"&gt;--convert&lt;/span&gt; components tests &lt;span class="nt"&gt;--folder&lt;/span&gt; ui/form
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Did you guess? The codemod performs the exact steps that you learned above.&lt;/p&gt;

</description>
      <category>ember</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>It's Time to Separate: Lint and Test</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Sat, 14 Jun 2025 06:15:15 +0000</pubDate>
      <link>https://dev.to/ijlee2/its-time-to-separate-lint-and-test-376b</link>
      <guid>https://dev.to/ijlee2/its-time-to-separate-lint-and-test-376b</guid>
      <description>&lt;p&gt;Today, you get to learn a pet peeve of mine, a rare instance where I never follow what Ember blueprints say since 2020.&lt;/p&gt;

&lt;p&gt;Since &lt;a href="https://github.com/ember-cli/ember-cli/blob/v3.17.0/blueprints/app/files/package.json#L19" rel="noopener noreferrer"&gt;version 3.17.0&lt;/a&gt;, &lt;code&gt;ember-cli&lt;/code&gt; generates apps and addons with a test script that—weirdly—checks files for lint and format errors.&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;package.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(generated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ember-cli@&lt;/span&gt;&lt;span class="mf"&gt;6.4&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"scripts"&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;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ember build --environment=production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prettier . --cache --write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"concurrently &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;pnpm:lint:*(!fix)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; --names &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;lint:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; --prefixColors auto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:css"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stylelint &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;**/*.css&lt;/span&gt;&lt;span class="se"&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;"lint:css:fix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"concurrently &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;pnpm:lint:css -- --fix&lt;/span&gt;&lt;span class="se"&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;"lint:fix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"concurrently &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;pnpm:lint:*:fix&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; --names &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;fix:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; --prefixColors auto &amp;amp;&amp;amp; pnpm format"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prettier . --cache --check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:hbs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ember-template-lint ."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:hbs:fix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ember-template-lint . --fix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eslint . --cache"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:js:fix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eslint . --fix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc --noEmit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"concurrently &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;pnpm:lint&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;pnpm:test:*&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; --names &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;lint,test:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; --prefixColors auto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:ember"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ember test"&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;p&gt;The lack of separation between lint and test resulted in 3 side effects that the &lt;a href="https://github.com/ember-cli/ember-cli/pull/9009" rel="noopener noreferrer"&gt;initial implementation hadn't accounted for&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In CI, &lt;a href="https://github.com/ember-cli/ember-cli/blob/v6.4.0/blueprints/app/files/.github/workflows/ci.yml#L52-L53" rel="noopener noreferrer"&gt;the lint command runs twice&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;In &lt;a href="https://github.com/ember-cli/ember-cli/blob/v6.4.0/blueprints/app/files/README.md?plain=1#L33-L36" rel="noopener noreferrer"&gt;&lt;code&gt;README&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/ember-cli/ember-cli/blob/v6.4.0/blueprints/addon/files/CONTRIBUTING.md?plain=1#L16-L17" rel="noopener noreferrer"&gt;&lt;code&gt;CONTRIBUTING&lt;/code&gt;&lt;/a&gt;, the test commands show an incongruence: People can run &lt;code&gt;pnpm test&lt;/code&gt; and &lt;code&gt;pnpm test:ember --server&lt;/code&gt;. Wouldn't &lt;code&gt;pnpm test --server&lt;/code&gt; make more sense?&lt;sup&gt;1&lt;/sup&gt; They neglect to mention that the &lt;code&gt;test&lt;/code&gt; command lints files, which has occasionally led to questions on Ember Discord.&lt;/li&gt;
&lt;li&gt;We need to add the query parameter &lt;code&gt;nolint&lt;/code&gt; to the &lt;code&gt;/tests&lt;/code&gt; URL, if we don't want lint results from addons to double the test report size. This issue was prevalent when we had depended on &lt;a href="https://github.com/ember-cli/ember-cli-eslint" rel="noopener noreferrer"&gt;ember-cli-eslint&lt;/a&gt; and &lt;a href="https://github.com/ember-template-lint/ember-cli-template-lint" rel="noopener noreferrer"&gt;ember-cli-template-lint&lt;/a&gt;. We can still get a mixed report due to &lt;a href="https://github.com/salsify/ember-cli-dependency-lint" rel="noopener noreferrer"&gt;ember-cli-dependency-lint&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;{
  "scripts": {
&lt;span class="gd"&gt;-     "test": "concurrently \"pnpm:lint\" \"pnpm:test:*\" --names \"lint,test:\" --prefixColors auto",
-     "test:ember": "ember test"
&lt;/span&gt;&lt;span class="gi"&gt;+     "test": "ember test"
&lt;/span&gt;  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In v1 addons, and in &lt;a href="https://github.com/ijlee2/embroider-toolbox/blob/1.2.0/packages/create-v2-addon-repo/src/blueprints/test-app/package.json#L27-L28" rel="noopener noreferrer"&gt;apps that test v2 addons&lt;/a&gt;, I use these scripts:&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;"scripts"&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;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ember test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:ember-compatibility"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ember try:one"&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;p&gt;Note, the test script for &lt;a href="https://github.com/ember-cli/ember-try" rel="noopener noreferrer"&gt;ember-try&lt;/a&gt; uses &lt;code&gt;try:one&lt;/code&gt; to run a single scenario, instead of &lt;code&gt;try:each&lt;/code&gt; to run all. This way, we can &lt;a href="https://github.com/ijlee2/embroider-toolbox/blob/1.2.0/packages/create-v2-addon-repo/src/blueprints/.github/workflows/ci.yml#L117-L127" rel="noopener noreferrer"&gt;run our scenarios in CI in parallel&lt;/a&gt;, and locally test a failing scenario quickly. Separation FTW.&lt;/p&gt;

&lt;p&gt;While I could continue to apply these changes to my projects manually, it'd be better if we update the blueprints to reflect the Ember ecosystem of now and our improved knowledge of CI since 2020.&lt;/p&gt;

&lt;p&gt;There isn't yet an &lt;a href="https://github.com/emberjs/rfcs" rel="noopener noreferrer"&gt;RFC&lt;/a&gt; that highlights the issues above and &lt;a href="https://github.com/ember-cli/ember-cli/pull/10663" rel="noopener noreferrer"&gt;proposes change&lt;/a&gt;. I think this is because we've gotten used to the problems and think less over time in the shoes of a newcomer. Let's make Ember projects a delight to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;You will get an error when you run &lt;code&gt;pnpm test --server&lt;/code&gt;. The error message says: &lt;code&gt;Unknown option: 'server'&lt;/code&gt;. Unknown to whom? &lt;code&gt;pnpm&lt;/code&gt;, &lt;code&gt;concurrently&lt;/code&gt;, &lt;code&gt;pnpm:lint&lt;/code&gt;, or &lt;code&gt;pnpm:test:*&lt;/code&gt;?&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ember</category>
      <category>javascript</category>
      <category>testing</category>
      <category>beginners</category>
    </item>
    <item>
      <title>It’s Time to Separate: Lint and Format</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Mon, 02 Jun 2025 11:54:22 +0000</pubDate>
      <link>https://dev.to/ijlee2/its-time-to-separate-lint-and-format-1oef</link>
      <guid>https://dev.to/ijlee2/its-time-to-separate-lint-and-format-1oef</guid>
      <description>&lt;p&gt;In the It's Time to Separate series, I'll show you how separating concerns helps us simplify code. And that separation can occur in many places.&lt;/p&gt;

&lt;p&gt;Little advertised news from January: Ember CLI removed &lt;a href="https://github.com/prettier/eslint-plugin-prettier" rel="noopener noreferrer"&gt;eslint-plugin-prettier&lt;/a&gt; and &lt;a href="https://github.com/prettier/stylelint-prettier" rel="noopener noreferrer"&gt;stylelint-prettier&lt;/a&gt; from its blueprints, &lt;a href="https://github.com/ember-cli/ember-cli/pull/10596" rel="noopener noreferrer"&gt;adding instead two scripts&lt;/a&gt; to run &lt;code&gt;prettier&lt;/code&gt;. I was skeptical when the &lt;a href="https://github.com/emberjs/rfcs/blob/master/text/1055-vanilla-prettier-setup-in-blueprints.md" rel="noopener noreferrer"&gt;RFC&lt;/a&gt; had come out. Why change things when these plugins have worked well for years? Isn't it, for people who maintain many projects like me, huge effort, little reward?&lt;/p&gt;

&lt;p&gt;Then I realized last week, separating formatting from linting is indeed a good thing. Not just cause of the performance gain that's usually cited. But because it helps us reexamine past approaches and come up with simpler ones that will stand the test of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. How to migrate
&lt;/h2&gt;

&lt;p&gt;Separation was painless. You could run &lt;a href="https://github.com/ember-cli/ember-cli-update" rel="noopener noreferrer"&gt;ember-cli-update&lt;/a&gt; (&lt;a href="https://blog.emberjs.com/ember-released-6-3" rel="noopener noreferrer"&gt;set the version to 6.3.0&lt;/a&gt; or higher). But likely you can't cause you have Ember v5 or less, and didn't address &lt;a href="https://deprecations.emberjs.com/v5.x" rel="noopener noreferrer"&gt;all the deprecations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You don't need to. &lt;em&gt;Separate&lt;/em&gt; the Prettier migration from the Ember update. Here are the steps, assuming that your project didn't deviate much from the blueprints. If you get lost, you can look at &lt;a href="https://github.com/ijlee2/frontend-configs" rel="noopener noreferrer"&gt;my open-sourced configs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;1. In &lt;code&gt;package.json&lt;/code&gt;, remove &lt;code&gt;eslint-plugin-prettier&lt;/code&gt; and &lt;code&gt;stylelint-prettier&lt;/code&gt;. Update the scripts to separate formatting and linting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* package.json */
{
  "scripts": {
&lt;span class="gi"&gt;+     "format": "prettier . --cache --write",
&lt;/span&gt;    "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"",
    "lint:css": "stylelint \"**/*.css\" --cache",
    "lint:css:fix": "pnpm lint:css --fix",
&lt;span class="gd"&gt;-     "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\"",
&lt;/span&gt;&lt;span class="gi"&gt;+     "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" &amp;amp;&amp;amp; pnpm format",
+     "lint:format": "prettier . --cache --check",
&lt;/span&gt;    "lint:js": "eslint . --cache",
    "lint:js:fix": "pnpm lint:js --fix"
  },
  "devDependencies": {
    "concurrently": "...",
    "eslint": "...",
    "eslint-config-prettier": "...",
&lt;span class="gd"&gt;-     "eslint-plugin-prettier": "...",
&lt;/span&gt;    "prettier": "...",
    "prettier-plugin-ember-template-tag": "..."
    "stylelint": "...",
&lt;span class="gd"&gt;-     "stylelint-prettier": "..."
&lt;/span&gt;  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have &lt;a href="https://crunchingnumbers.live/2025/02/11/shared-lint-configs/" rel="noopener noreferrer"&gt;shared configurations&lt;/a&gt; for &lt;code&gt;eslint&lt;/code&gt; and &lt;code&gt;stylelint&lt;/code&gt;, you can remove &lt;code&gt;prettier&lt;/code&gt; from peer dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* package.json (eslint config) */
{
  "peerDependencies": {
    "eslint": "^9.0.0",
&lt;span class="gd"&gt;-     "prettier": "^3.0.0",
&lt;/span&gt;    "typescript": "^5.0.0"
  },
  "peerDependenciesMeta": {
    "typescript": {
      "optional": true
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2. Remove &lt;code&gt;eslint-plugin-prettier&lt;/code&gt; from the &lt;code&gt;eslint&lt;/code&gt; configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* eslint.config.mjs (eslint@v9) */
&lt;span class="p"&gt;import eslint from '@eslint/js';
&lt;/span&gt;&lt;span class="gd"&gt;- import eslintPluginPrettier from 'eslint-plugin-prettier/recommended';
&lt;/span&gt;&lt;span class="gi"&gt;+ import eslintConfigPrettier from 'eslint-config-prettier';
&lt;/span&gt;&lt;span class="p"&gt;import tseslint from 'typescript-eslint';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export default tseslint.config(
&lt;/span&gt;  eslint.configs.recommended,
&lt;span class="gd"&gt;-   eslintPluginPrettier,
&lt;/span&gt;&lt;span class="gi"&gt;+   eslintConfigPrettier,
&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* .eslintrc.cjs (eslint@v8) */
'use strict';
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;module.exports = {
&lt;/span&gt;  extends: [
    'eslint:recommended',
&lt;span class="gd"&gt;-     'plugin:prettier/recommended',
&lt;/span&gt;&lt;span class="gi"&gt;+     'prettier',
&lt;/span&gt;  ],
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note, we added &lt;code&gt;eslint-config-prettier&lt;/code&gt; to turn off &lt;code&gt;eslint&lt;/code&gt; rules that may conflict with &lt;code&gt;prettier&lt;/code&gt;. &lt;code&gt;eslint-plugin-prettier&lt;/code&gt; used to do this for us.&lt;/p&gt;

&lt;p&gt;Also worth knowing, &lt;code&gt;eslint-plugin-prettier&lt;/code&gt; had to be the last plugin in order to override rules from the preceding ones. You'll be fine if you list &lt;code&gt;eslint-config-prettier&lt;/code&gt; where &lt;code&gt;eslint-plugin-prettier&lt;/code&gt; used to be.&lt;/p&gt;

&lt;p&gt;3. Remove &lt;code&gt;stylelint-prettier&lt;/code&gt; from the &lt;code&gt;stylelint&lt;/code&gt; configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* .stylelintrc.mjs */
&lt;span class="p"&gt;export default {
&lt;/span&gt;&lt;span class="gd"&gt;-   extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'],
&lt;/span&gt;&lt;span class="gi"&gt;+   extends: ['stylelint-config-standard'],
&lt;/span&gt;};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4. Run &lt;code&gt;install&lt;/code&gt; to update your dependencies. Run &lt;code&gt;lint:css&lt;/code&gt; and &lt;code&gt;lint:js&lt;/code&gt; to check that files can be linted.&lt;/p&gt;

&lt;p&gt;5. Prettier now checks more files than it used to via linter plugins. Run &lt;code&gt;lint:format&lt;/code&gt; to identify files that shouldn't be formatted. List them in &lt;code&gt;.prettierignore&lt;/code&gt;. Here is the starter config for Ember apps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# unconventional js
/blueprints/*/files/

# compiled output
/dist/

# misc
/coverage/
!.*
.*/
*.html
pnpm-lock.yaml

# specific to my package
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note, &lt;code&gt;*.html&lt;/code&gt; was added as a temporary fix for &lt;a href="https://github.com/prettier/prettier/issues/15336" rel="noopener noreferrer"&gt;a bug in Prettier&lt;/a&gt;. &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; isn't needed if the package is a workspace in a monorepo.&lt;/p&gt;

&lt;p&gt;Migration complete! What's nice about the names &lt;code&gt;format&lt;/code&gt; and &lt;code&gt;lint:format&lt;/code&gt; is backward compatibility. Just like before, developers need to know only 3 scripts to review their work:&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;# Check errors&lt;/span&gt;
pnpm lint

&lt;span class="c"&gt;# Fix errors&lt;/span&gt;
pnpm lint:fix

&lt;span class="c"&gt;# Run tests&lt;/span&gt;
pnpm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. How to format hbs
&lt;/h2&gt;

&lt;p&gt;Ember aficionados may notice a gap. What about Glimmer templates?&lt;/p&gt;

&lt;p&gt;Until now, we had to use &lt;a href="https://github.com/ember-template-lint/ember-template-lint-plugin-prettier" rel="noopener noreferrer"&gt;ember-template-lint-plugin-prettier&lt;/a&gt; to format &lt;code&gt;*.hbs&lt;/code&gt; files. This is a bit strange, because &lt;a href="https://dev.toPrettier%20natively%20supports%20Handlebars"&gt;Prettier natively supports Handlebars&lt;/a&gt; since May 2021.&lt;/p&gt;

&lt;p&gt;The plugin also comes with a few issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It uglifies code inside an &lt;code&gt;hbs&lt;/code&gt; tag (i.e. wrong indentations in rendering tests, Storybook stories).&lt;/li&gt;
&lt;li&gt;It needs to dynamically load &lt;code&gt;prettier&lt;/code&gt; and use a hook from &lt;code&gt;ember-template-lint&lt;/code&gt; to format &lt;code&gt;*.hbs&lt;/code&gt;. Due to strong coupling, it will fall behind if &lt;code&gt;prettier&lt;/code&gt; or &lt;code&gt;ember-template-lint&lt;/code&gt; makes a breaking change to their API.&lt;/li&gt;
&lt;li&gt;Prettier recommends not running prettier through a linter plugin. Partly why Ember CLI removed the &lt;code&gt;eslint&lt;/code&gt; and &lt;code&gt;stylelint&lt;/code&gt; plugins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to the separation, we can see that all we need now is a Prettier plugin that formats hbs tags.&lt;/p&gt;

&lt;p&gt;Enter &lt;a href="https://github.com/ijlee2/prettier-plugin-ember-hbs-tag" rel="noopener noreferrer"&gt;prettier-plugin-ember-hbs-tag&lt;/a&gt;, a plugin that I wrote last week. Just like &lt;a href="https://github.com/ember-tooling/prettier-plugin-ember-template-tag" rel="noopener noreferrer"&gt;prettier-plugin-ember-template-tag&lt;/a&gt;, it has one job and doesn't need a linter to run. Here is the starter config:&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="cm"&gt;/* prettier.config.mjs */&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="na"&gt;plugins&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;prettier-plugin-ember-hbs-tag&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="s1"&gt;prettier-plugin-ember-template-tag&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="na"&gt;overrides&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;files&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;*.{cjs,cts,js,mjs,mts,ts}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;singleQuote&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="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;files&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;tests/**/*-test.{js,ts}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-hbs-tag&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;singleQuote&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;templateSingleQuote&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="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;files&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;*.{gjs,gts}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;singleQuote&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;templateSingleQuote&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="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;files&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;*.hbs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;printWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;singleQuote&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, remove &lt;code&gt;ember-template-lint-plugin-prettier&lt;/code&gt; from the &lt;code&gt;ember-template-lint&lt;/code&gt; config and uninstall the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* .template-lintrc.cjs */
'use strict';
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;module.exports = {
&lt;/span&gt;&lt;span class="gd"&gt;-   plugins: ['ember-template-lint-plugin-prettier'],
-   extends: ['recommended', 'ember-template-lint-plugin-prettier:recommended'],
-   overrides: [
-     {
-       files: ['**/*.{gjs,gts}'],
-       rules: {
-         prettier: 'off',
-       },
-     },
-     {
-       files: ['tests/**/*-test.{js,ts}'],
-       rules: {
-         prettier: 'off',
-       },
-     },
-   ],
&lt;/span&gt;&lt;span class="gi"&gt;+   extends: ['recommended'],
&lt;/span&gt;};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/* package.json */
{
  "devDependencies": {
    "ember-template-lint": "...",
&lt;span class="gd"&gt;-     "ember-template-lint-plugin-prettier": "...",
&lt;/span&gt;    "prettier": "...",
&lt;span class="gi"&gt;+     "prettier-plugin-ember-hbs-tag": "...",
&lt;/span&gt;    "prettier-plugin-ember-template-tag": "..."
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, you can format &lt;em&gt;everything&lt;/em&gt;. &lt;a href="https://github.com/ijlee2/prettier-plugin-ember-hbs-tag" rel="noopener noreferrer"&gt;Don't forget to star&lt;/a&gt;. ✨&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Even more separation
&lt;/h2&gt;

&lt;p&gt;Starting with &lt;code&gt;ember-template-lint@7.7.0&lt;/code&gt;, you can &lt;a href="https://github.com/ijlee2/ember-codemod-sort-invocations" rel="noopener noreferrer"&gt;sort invocations&lt;/a&gt;.&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="cm"&gt;/* .template-lintrc.cjs */&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;extends&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;recommended&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;rules&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;sort-invocations&lt;/span&gt;&lt;span class="dl"&gt;'&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="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;In order to fix a long-standing issue, &lt;code&gt;sort-invocations&lt;/code&gt; leaves formatting up to the two Prettier plugins. Stay tuned for the dedicated post.&lt;/p&gt;

</description>
      <category>ember</category>
      <category>prettier</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Fixing Package Dependencies</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Fri, 19 Jul 2024 09:28:47 +0000</pubDate>
      <link>https://dev.to/ijlee2/fixing-package-dependencies-5557</link>
      <guid>https://dev.to/ijlee2/fixing-package-dependencies-5557</guid>
      <description>&lt;p&gt;Both &lt;a href="https://github.com/embroider-build/embroider" rel="noopener noreferrer"&gt;Embroider&lt;/a&gt; and &lt;code&gt;pnpm&lt;/code&gt; ask that packages declare their dependencies correctly: List a dependency (if and only) if it is used.&lt;/p&gt;

&lt;p&gt;This is difficult to do when working on a large monorepo (consider an Ember app with many Ember addons and Node packages) that uses &lt;code&gt;yarn@v1&lt;/code&gt;. Developers can forget to update the &lt;code&gt;package.json&lt;/code&gt;'s, because the Ember app can build and run even when a dependency is missing, as long as it gets pulled in from another package.&lt;/p&gt;

&lt;p&gt;So neither build nor run can tell us if some package didn't declare its dependencies right. How else can we fix the &lt;code&gt;package.json&lt;/code&gt;'s so that we can introduce Embroider and &lt;code&gt;pnpm&lt;/code&gt;?&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Static code analysis
&lt;/h2&gt;

&lt;p&gt;Given a file, we can see which dependencies should be present, because we know how JavaScript and Ember work.&lt;/p&gt;

&lt;p&gt;For example, were a JavaScript (or TypeScript) file to show,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setupIntl&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="s1"&gt;ember-intl/test-support&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;setupRenderingTest&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;upstreamSetupRenderingTest&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="s1"&gt;ember-qunit&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setupRenderingTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hooks&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;upstreamSetupRenderingTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hooks&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="c1"&gt;// Additional setup for rendering tests can be done here.&lt;/span&gt;
  &lt;span class="nf"&gt;setupIntl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de-de&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;we would tell from the import statements that the package depends on &lt;code&gt;ember-intl&lt;/code&gt; and &lt;code&gt;ember-qunit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And, if a template file were to show,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;page-title&lt;/span&gt; &lt;span class="s2"&gt;"My App"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;WelcomePage&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;outlet&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;our knowledge of Ember and its addon ecosystem would direct us to &lt;code&gt;ember-page-title&lt;/code&gt;, &lt;code&gt;ember-welcome-page&lt;/code&gt;, and &lt;code&gt;ember-source&lt;/code&gt;, respectively.&lt;/p&gt;

&lt;p&gt;Even when things are implicit (e.g. ambiguity in double curly braces, module resolution, service injection), we can guess the origin of an entity (entities are components, helpers, modifiers, services, etc.) with high accuracy, thanks to Ember's strong conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Codemod
&lt;/h2&gt;

&lt;p&gt;Still, we shouldn't check every file in every package manually. That's time-consuming and error-prone.&lt;/p&gt;

&lt;p&gt;Instead, we write a codemod (really, a linter) using &lt;a href="https://github.com/ijlee2/codemod-utils" rel="noopener noreferrer"&gt;&lt;code&gt;@codemod-utils&lt;/code&gt;&lt;/a&gt;. For every package, the codemod parses what's relevant and creates a list of dependencies that should be present ("actual"). It then compares the list to that from &lt;code&gt;package.json&lt;/code&gt; ("expected").&lt;/p&gt;

&lt;p&gt;To analyze implicit code, there needs to be a list of known entities (a one-time creation), which maps every package that we want to consider to its entities. We can use a &lt;code&gt;Map&lt;/code&gt; to record that information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_ENTITIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&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;ember-intl&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="na"&gt;helpers&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;format-date&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="s1"&gt;format-list&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="s1"&gt;format-message&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="s1"&gt;format-number&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="s1"&gt;format-relative&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="s1"&gt;format-time&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="s1"&gt;t&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="na"&gt;services&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;intl&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="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-page-title&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="na"&gt;helpers&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;page-title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;services&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;page-title&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="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-welcome-page&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="na"&gt;components&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;welcome-page&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="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even explicit code like import statements aren't trivial to analyze. Take the following example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Route&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;@ember/routing/route&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fetch&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;fetch&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;When we don't provide the right context (i.e. this code is for Ember), the codemod would consider &lt;code&gt;@ember/routing&lt;/code&gt; and &lt;code&gt;fetch&lt;/code&gt; as dependencies, instead of &lt;code&gt;ember-source&lt;/code&gt; and (likely) &lt;code&gt;ember-fetch&lt;/code&gt;. The codemod should present its analysis in such a way that we can easily check for false positives and false negatives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Results for my-package-37

{
  missingDependencies: [
    'ember-asset-loader',
    'ember-truth-helpers'
  ],
  unusedDependencies: [
    '@babel/core',
    'ember-auto-import',
    'ember-cli-babel'
  ],
  unknowns: [
    'Service - host-router (addon/routes/registration.ts)',
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Results
&lt;/h2&gt;

&lt;p&gt;The codemod that I had built (in a couple of days) analyzed a production repo with 123 packages in 25 seconds. There were a total of 11,108 files, but the codemod knew to analyze only 5,506 of them (less than half). That's an average of 0.005 seconds/file and 0.20 seconds/package!&lt;/p&gt;

&lt;p&gt;To learn more about writing codemods, check out the main tutorial from &lt;a href="https://github.com/ijlee2/codemod-utils" rel="noopener noreferrer"&gt;&lt;code&gt;@codemod-utils&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ember</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>In 1 Year</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Thu, 20 Jul 2023 22:08:56 +0000</pubDate>
      <link>https://dev.to/ijlee2/in-1-year-25h0</link>
      <guid>https://dev.to/ijlee2/in-1-year-25h0</guid>
      <description>&lt;p&gt;Today, I'm going to show you how to create a platform so that you and your team can write code that is more maintainable and extensible.&lt;/p&gt;

&lt;p&gt;Since last year, at CLARK, we've been reducing our tech debt. Our main application is a monorepo with 190 packages, accrued over 7 or 8 years. We've been on Ember 3.28 for almost 2 years now. Trying to get to 4 hasn't been easy.&lt;/p&gt;

&lt;p&gt;These issues may sound familiar to you and I think it's a good thing to know that we don't have to face them alone. The &lt;a href="https://emberjs.com/survey/2022/#s03-at-work" rel="noopener noreferrer"&gt;Ember Community Survey in 2022&lt;/a&gt; estimated that 3 out of 4 work projects may be stuck on v3 or below. A follow-up survey in December indicated the lack of resource and outdated addons as main reasons.&lt;/p&gt;

&lt;p&gt;The question is, why are we finding ourselves in projects that are hard to maintain and extend? What can we do differently? I want to highlight possible solutions and give practical tips based on what my colleagues and I achieved over the last year.&lt;/p&gt;

&lt;p&gt;Keep in mind that the problems that I will describe are not specific to work or Ember v3, but more programming in general. The reason is, I want you to be able to apply these solutions in other contexts, e.g. in open-source projects like &lt;a href="https://github.com/adopted-ember-addons" rel="noopener noreferrer"&gt;adopted-ember-addons&lt;/a&gt; or when it's time to update Ember from 4 to 5.&lt;/p&gt;

&lt;p&gt;So, fangen wir mal an. Let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. In increments
&lt;/h2&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F03.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F03.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you have to update a project that hasn't been maintained, you're going to feel, at first, overwhelmed, hopeless. How in the world do we get out of this mess? The key to doing so—and this is the most important lesson from my talk: Do it &lt;em&gt;in increments&lt;/em&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%2F0uegr9s5qicqb3x61civ.gif" 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%2F0uegr9s5qicqb3x61civ.gif" alt="A yarn is shown to be gradually untangled"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Imagine a yarn that is wildly tangled up. The yarn is supposed to be just 1 simple thread, but it is a chaos and you can't tell where it starts and where it ends. Now, if you try to untangle the yarn by applying force in every direction, all at once, you're going to fail and make a bigger mess. But, if you give small little tugs, one at a time, sooner or later you'll find yourself that 1 simple thread.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F08.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F08.png" alt="4 contours, 3 consecutive arrows, and a target to illustrate how iterative methods work."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This incremental approach is also what mathematicians favor. Global algorithms like finding the inverse of a function work well in theory but are very limited in practice. &lt;a href="https://en.wikipedia.org/wiki/Iterative_method" rel="noopener noreferrer"&gt;Iterative methods&lt;/a&gt;, on the other hand—taking a step in one direction, a step in another, and so on, until we converge to the right solution—are powerful because we can apply them to many different situations. They are also efficient and will end up saving us time.&lt;/p&gt;

&lt;p&gt;So what does an incremental approach mean for us developers? It means two things.&lt;/p&gt;

&lt;p&gt;First, when it's time to update a project, we have to avoid creating a plan that spans weeks or months. We also have to avoid planning down to the smallest detail. Now, I'm not saying, have no plan and just wing it. What I'm pointing out is that your code is alive. It will never stay still because someone on your team will add a feature, or some package that you depend on will have a release. The assumptions for how to update your project will change quickly, and you will have to adapt quickly as well.&lt;/p&gt;

&lt;p&gt;Second, an incremental approach means allowing mistakes. The right solution won't be obvious from the start, especially because the project hasn't been maintained. A pull request to update the project may introduce bugs, but that's okay, we can fix them in the next one. This may sound obvious but, if we take &lt;em&gt;no&lt;/em&gt; steps because we fear of breaking the app, then we will get nowhere. Only by taking small steps and iterating on the solution can we make impossible possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  1a. Code metrics
&lt;/h3&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F09.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F09.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You rated me a six. I was like, "Damn."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The rest of my talk is devoted to showing which small steps you can take. But, before I do that, I'll share my thoughts on code metrics: &lt;em&gt;Should&lt;/em&gt; we measure the improvements that we make?&lt;/p&gt;

&lt;p&gt;In math, a &lt;a href="https://en.wikipedia.org/wiki/Metric_space" rel="noopener noreferrer"&gt;metric&lt;/a&gt; is a number that describes a phenomenon without bias. For example, you are 10 centimeters taller than I, or the train was late by 5 minutes. One day, programmers, inspired by math, came up with numbers to describe &lt;a href="https://en.wikipedia.org/wiki/Software_metric" rel="noopener noreferrer"&gt;code quality&lt;/a&gt;. How many lines of code are there? How many components and routes do we have? And so on.&lt;/p&gt;

&lt;p&gt;The problem is, your code is alive and the assumptions for your project change from one day to another. You have to somehow ignore the changes that weren't in your control by normalizing the metric. Maybe divide the metric by the number of lines of code, or divide by the square root or the logarithm. Whatever you can think of.&lt;/p&gt;

&lt;p&gt;You're starting to see that we're fidgeting numbers and adding personal bias, when a metric is supposed to be objective. I claim that it's impossible to measure the &lt;em&gt;change&lt;/em&gt; of a code metric—in other words, how much the code improved over time. We can only measure what is now, at this moment.&lt;/p&gt;

&lt;p&gt;Long story short, don't worry about code metrics. Just do the right thing and improve that code that you've wanted to.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The right code
&lt;/h2&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F10.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F10.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next question is, well, what is the right thing? What does it look like? The answer is going to depend on which aspects of code we care about. To help narrow the answer, we will focus on two: maintainability and extensibility.&lt;/p&gt;

&lt;p&gt;A code that we can maintain and extend exhibits 3 characteristics: It has a minimum API, it separates concerns, and it has few dependencies. In math, we say that these are &lt;a href="https://en.wikipedia.org/wiki/Necessity_and_sufficiency" rel="noopener noreferrer"&gt;necessary conditions&lt;/a&gt;. This means, if your code doesn't meet one of these conditions, it cannot be maintained or extended. Think of these as a checklist, where each item tells you which small steps you can take.&lt;/p&gt;

&lt;p&gt;To make things more concrete, I will use &lt;a href="https://guides.emberjs.com/release/components/introducing-components" rel="noopener noreferrer"&gt;reusable components&lt;/a&gt;, something that we are familiar with and use on a daily basis.&lt;/p&gt;

&lt;h3&gt;
  
  
  2a. The right code has minimum API
&lt;/h3&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F12.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F12.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An &lt;a href="https://en.wikipedia.org/wiki/API" rel="noopener noreferrer"&gt;API&lt;/a&gt; (application programming interface) defines a boundary between two code and rules for communicating with each other. When the interface is good, the code is easy to use, maintain, and extend. When the interface is bad, the code becomes a hazard and can stop your project at some point.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F13.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F13.png" alt="Two rectangles that represent the consumer and the reusable component. The rectangles are connected by a double-pointed arrow, which represents the API."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A common mistake that I see in reusable components is supporting too many cases. That is, the component ended up with a large API by allowing many &lt;a href="https://guides.emberjs.com/release/components/component-arguments-and-html-attributes" rel="noopener noreferrer"&gt;arguments&lt;/a&gt;. This happens when we try to predict the future and overdesign things, or when we try to quickly fix something with an &lt;code&gt;if&lt;/code&gt;-statement (if there is this new argument, I'm gonna do something else).&lt;/p&gt;

&lt;p&gt;What we tend to ignore is how every case increases &lt;a href="https://en.wikipedia.org/wiki/Programming_complexity" rel="noopener noreferrer"&gt;complexity&lt;/a&gt;. Not linearly but, I claim, quadratically or worse because of the &lt;a href="https://en.wikipedia.org/wiki/Combinatorics" rel="noopener noreferrer"&gt;combinatorial&lt;/a&gt; effect—how arguments interact with one another. We can assume that the higher the complexity, the more likely that the component is untested and unmaintained.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F14.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F14.png" alt="A humorous, fictitious photo of eierlegende Wollmilchsau, a hybrid animal with body parts from a chicken, sheep, cow, and pig."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Back in Germany, we have this amazing animal that's going to sustain the future, called &lt;a href="https://de.wikipedia.org/wiki/Eierlegende_Wollmilchsau" rel="noopener noreferrer"&gt;eierlegende Wollmilchsau&lt;/a&gt;. It can give us eggs, wool, milk, meat, &lt;em&gt;and&lt;/em&gt; companionship. It can do everything that we want, because it does not exist. It's an idiom like "Jack of all trades, master of none."&lt;/p&gt;

&lt;p&gt;So the lesson is, design simple things. Got it. But what if I already have a component with a large API? How do I simplify it?&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F15.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F15.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key to answering this question is researching your current use cases. Find out which features were almost always used and which maybe once or twice. The code for the rarely used features? See if you can delete them.&lt;/p&gt;

&lt;p&gt;By doing so, you reduce complexity and the chance that your component will cause an issue. Also, by removing code, you can cause a chain reaction of additional refactors. Remember, the way to untangle a yarn is to give small little tugs.&lt;/p&gt;

&lt;p&gt;Now, what if a feature was used once, but you have to support it? I'd create another component, maybe through composition. The idea is, to treat this feature as an exception and not the norm. When designing reusable components, I want you to &lt;a href="https://en.wikipedia.org/wiki/Pareto_principle" rel="noopener noreferrer"&gt;target the 80%&lt;/a&gt; and not give in to the other 20%.&lt;/p&gt;

&lt;h3&gt;
  
  
  2b. The right code separates concerns
&lt;/h3&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F16.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F16.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recall that an API draws a boundary between two code. In the best case, each side trusts the other to perform only certain tasks and no others. When this happens, the tests for each side are simpler.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F17.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F17.png" alt="Two rectangles that represent the consumer and the reusable component. The rectangles are connected by a double-pointed arrow, which represents the API."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The question is, when we design a reusable component, how should we separate responsibilities? Which tasks belong to the reusable component and which to the consumer, whether that's a component or route?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ijlee2/ember-workshop" rel="noopener noreferrer"&gt;In my experience&lt;/a&gt;, we can maintain and extend the reusable component when it handles these 3 aspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://emberjs-1.gitbook.io/ember-component-patterns" rel="noopener noreferrer"&gt;Accessibility&lt;/a&gt; (so the consumer doesn't have to be an expert in it)&lt;/li&gt;
&lt;li&gt;Styling (making sure that things inside the container look right)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mainmatter/ember-test-selectors" rel="noopener noreferrer"&gt;Test selectors&lt;/a&gt; (what should be tested and how are the selectors named?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meanwhile, the consumer must provide these three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data and translations (what should the component render?)&lt;/li&gt;
&lt;li&gt;Margin and padding for the container (so the component can play nice with others)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://guides.emberjs.com/release/in-depth-topics/patterns-for-actions" rel="noopener noreferrer"&gt;Callback functions&lt;/a&gt; (when a user takes an action, what should the component do?)&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F19.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F19.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How I separated styling—namely, the container doesn't set its margin and padding, but the consumer does with an extra &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;—may come as a surprise. I learned this from Sean Massa and Trek Glowacki a few years ago, and find that it really helps us reuse and refactor components. The rationale is, reusable components should only care about what happens inside. We encounter this idea also in &lt;a href="https://github.com/ijlee2/ember-container-query" rel="noopener noreferrer"&gt;container queries&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F20.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F20.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another thing to note here is data and translations. Before &lt;a href="https://blog.emberjs.com/ember-3-25-released" rel="noopener noreferrer"&gt;Ember 3.25&lt;/a&gt;, we would have had to use a bunch of arguments to pass these down. Now, thanks to &lt;a href="https://guides.emberjs.com/release/components/block-content" rel="noopener noreferrer"&gt;named blocks&lt;/a&gt;, the reusable component can focus on the layout, while the consumer on the content. Named blocks are one of my favorite features of Ember.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F21.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F21.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A feature of Ember that I want you to use with caution is &lt;a href="https://guides.emberjs.com/release/components/component-arguments-and-html-attributes" rel="noopener noreferrer"&gt;...attributes&lt;/a&gt;, as they can easily destroy separation of concerns. I'll give you 2 examples.&lt;/p&gt;

&lt;p&gt;One time, I tried to replace flex with grid to simplify a reusable component, only to find out, many of the consumers had passed the &lt;code&gt;class&lt;/code&gt; attribute and had overwritten the flex properties. Thanks to splattributes, implementation got leaked and the consumers are now forcing me to keep using flex.&lt;/p&gt;

&lt;p&gt;Another problem that I observed at CLARK is too many test selectors, because many consumers had passed their own. When the same DOM element is referred to in 7 different ways, refactoring the reusable component becomes tedious. Suddenly, I have to update multiple test files in different packages.&lt;/p&gt;

&lt;p&gt;In general, I think &lt;code&gt;...attributes&lt;/code&gt; is a sign that the component wasn't designed right. Maybe it should ask the consumer to, instead, use arguments to define styles, named blocks to customize content, and test helpers to write tests. I want you to use the right tools to solve the right problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  2c. The right code depends on few
&lt;/h3&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F22.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F22.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just like arguments, every package that we install increases the chance that something goes wrong. But unlike arguments, we don't really have control over packages. If the package author doesn't do releases or they make breaking changes, our code can become stuck in time.&lt;/p&gt;

&lt;p&gt;Now, I'm not saying, write every code yourself and have zero dependencies. Instead, ask yourself: Is their code more &lt;em&gt;stable&lt;/em&gt; than mine? To me, stable doesn't mean, there's a 1.0 release. It means, the package is well-written, -documented, -tested, and -supported. You can install the package only if the answer is yes.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F23.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F23.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, at times, you will need to install a package that is not stable. If so, try to &lt;a href="https://en.wikipedia.org/wiki/Wrapper_function" rel="noopener noreferrer"&gt;wrap the code&lt;/a&gt; that you need, then write tests to document the wrapper's input and output. This way, if the package turns out to be no go, you will have to replace the code in only 1 place: the wrapper.&lt;/p&gt;

&lt;p&gt;I'll tell you about a mistake that we had made at CLARK and how it's now affecting us with upgrading Ember. (It's a funny story and you're going to laugh, because you're not affected.)&lt;/p&gt;

&lt;p&gt;We use &lt;a href="https://github.com/adopted-ember-addons/ember-file-upload" rel="noopener noreferrer"&gt;ember-file-upload&lt;/a&gt; and I noticed, we are on &lt;code&gt;5.0.0-beta&lt;/code&gt; when the latest is 8-something and I can't update Ember to 4 without updating &lt;code&gt;ember-file-upload&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;Well, it turns out, we came up with like 10 different ways to render &lt;code&gt;&amp;lt;FileUpload&amp;gt;&lt;/code&gt; and, since the beta, the addon changed its API and styling drastically. So now, we have to fix deprecations, visual regressions, and failing tests for every one of these cases. If only we had come up with a wrapper component, tja.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Solid foundation
&lt;/h2&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F24.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F24.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To help people design code right, we need a strong foundation—things like simple lint and test strategies, up-to-date dependencies, and short build and rebuild times. Replacing the foundation is a comparatively large task, but it's also something that we will have to do only once in a while.&lt;/p&gt;

&lt;p&gt;The question is, for an &lt;em&gt;existing&lt;/em&gt; project, how do we replace the foundation without stopping everything else? The solution, again, is in increments.&lt;/p&gt;

&lt;h3&gt;
  
  
  3a. Overhauling lint
&lt;/h3&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F25.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F25.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A year ago, at CLARK, every package had different linter configurations, so we couldn't update packages like &lt;code&gt;eslint&lt;/code&gt; and &lt;code&gt;typescript&lt;/code&gt;. Furthermore, how we asked our developers to lint files was different from how we asked our CI.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F26.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F26.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fast forward to now, our project is set up like this: There are only 3 scripts for developers to remember because every package has them: &lt;code&gt;lint&lt;/code&gt;, &lt;code&gt;lint:fix&lt;/code&gt;, and &lt;code&gt;test&lt;/code&gt;. CI runs the exact same scripts so we can easily reproduce issues locally.&lt;/p&gt;

&lt;p&gt;Second, we use the flag &lt;code&gt;--cache&lt;/code&gt; and the package &lt;a href="https://github.com/open-cli-tools/concurrently" rel="noopener noreferrer"&gt;concurrently&lt;/a&gt; so that we can lint files faster and more exhaustively.&lt;/p&gt;

&lt;p&gt;Lastly, we have limited resource so we rely on the default as much as possible. Things like &lt;a href="https://github.com/ember-cli/ember-cli/tree/master/blueprints" rel="noopener noreferrer"&gt;blueprints from ember-cli&lt;/a&gt; and official plugins like &lt;code&gt;@tsconfig/ember&lt;/code&gt;. We adopt 3rd-party plugins only if they are stable in the sense that I mentioned earlier.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tsconfig/bases/blob/main/bases/ember.json" rel="noopener noreferrer"&gt;@tsconfig/ember&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/eslint-community/eslint-plugin-n" rel="noopener noreferrer"&gt;eslint-plugin-n&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/lydell/eslint-plugin-simple-import-sort" rel="noopener noreferrer"&gt;eslint-plugin-simple-import-sort&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/infctr/eslint-plugin-typescript-sort-keys" rel="noopener noreferrer"&gt;eslint-plugin-typescript-sort-keys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hudochenkov/stylelint-order" rel="noopener noreferrer"&gt;stylelint-order&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here is how we changed linting across 190 packages. It turns out, packages are not equal. Leaf-node packages (packages that don't depend on others) were actually seldom worked on, so we updated them all at once. Packages for a business domain belong together so they were updated together.&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%2Fapurx91zlnbsbka4851v.gif" 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%2Fapurx91zlnbsbka4851v.gif" alt="We can change linting in groups of packages"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a single day, we updated &lt;code&gt;eslint&lt;/code&gt; to &lt;code&gt;v8&lt;/code&gt; and reset all configurations by bypassing CI. We asked &lt;code&gt;lint:js&lt;/code&gt; to return code 0, an unconditional success. Afterwards—again, in groups of packages—we reverted the scripts, ran auto-fix, and ignored errors that couldn't be fixed.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F31.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F31.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this divide-and-conquer strategy, it took me (one person) about 10 pull requests and no more than 5 days to introduce a change.&lt;/p&gt;

&lt;h3&gt;
  
  
  3b. Spotting blockers
&lt;/h3&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F32.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F32.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, we look out for deprecations and outdated packages that can block us from updating more critical dependencies like &lt;code&gt;ember-source&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F33.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F33.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To find deprecations, we can use &lt;a href="https://github.com/mixonic/ember-cli-deprecation-workflow" rel="noopener noreferrer"&gt;ember-cli-deprecation-workflow&lt;/a&gt; and create a to-do list. The addon does require that we have enough tests to avoid false negatives. Later, I'll show you how you can write simple tests when there aren't any. Another approach is to run the app and check &lt;a href="https://github.com/emberjs/ember-inspector" rel="noopener noreferrer"&gt;Ember Inspector&lt;/a&gt;'s Deprecations tab.&lt;/p&gt;

&lt;p&gt;Here are 3 deprecations that will likely affect many projects. You can visit &lt;a href="https://deprecations.emberjs.com" rel="noopener noreferrer"&gt;deprecations.emberjs.com&lt;/a&gt; to learn more.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;implicit-injections&lt;/code&gt; (v4)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;this-property-fallback&lt;/code&gt; (v4)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;routing.transition-methods&lt;/code&gt; (v5)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also gathered a list of important packages and the minimum version that you want to reach. It's important to update &lt;code&gt;ember-auto-import&lt;/code&gt; and &lt;code&gt;ember-modifier&lt;/code&gt; now, because more addons will move on to support &lt;a href="https://github.com/embroider-build/embroider" rel="noopener noreferrer"&gt;Embroider&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/embroider-build/ember-auto-import" rel="noopener noreferrer"&gt;ember-auto-import@2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-modifier/ember-modifier" rel="noopener noreferrer"&gt;ember-modifier@3.2.7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-template-lint/ember-template-lint" rel="noopener noreferrer"&gt;ember-template-lint@5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/eslint/eslint" rel="noopener noreferrer"&gt;eslint@8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/TypeScript" rel="noopener noreferrer"&gt;typescript@4.8.2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3c. Short (re)build
&lt;/h3&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F36.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F36.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To help people iterate on a solution many times, we need builds and rebuilds to be fast. A forewarning: High performance optimization isn't my expertise so I cannot give you definitive answers. Nonetheless, I'll share what we did at CLARK, which seemed to help lower the times. Maybe they can help you too.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F37.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F37.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We ended up with 190 packages because of premature abstractions. By combining packages and removing dead code, we are now down to 150. In the process, we removed a few cyclic dependencies by creating a leaf-node package.&lt;/p&gt;

&lt;p&gt;Now, if there is a component used by many packages, see if you can simplify it. You can, for example, make it &lt;a href="https://api.emberjs.com/ember/release/functions/@ember%2Fcomponent%2Ftemplate-only/templateOnly" rel="noopener noreferrer"&gt;template-only&lt;/a&gt; and replace older syntax with newer ones.&lt;/p&gt;

&lt;p&gt;An ongoing project for us is to declare dependencies correctly so that we can adopt Embroider. Because we use &lt;code&gt;yarn&lt;/code&gt; to manage the monorepo, many packages that had been created by copy-paste listed wrong dependencies. You can find unused dependencies by searching code for how they would have been used.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F38.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F38.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, if you have &lt;a href="https://github.com/ember-cli/ember-cli/releases/tag/v3.15.0" rel="noopener noreferrer"&gt;ember-cli@3.15&lt;/a&gt; or higher, there's a hidden feature that can make rebuilds faster. In &lt;code&gt;ember-cli-build.js&lt;/code&gt;, simply set &lt;code&gt;BROCCOLI_ENABLED_MEMOIZE&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;. This happens by default in Embroider projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Solving together
&lt;/h2&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F39.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F39.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My colleagues and I are in this lucky situation, where I can maintain code full-time, but that's still a bus factor of 1. It's important that I share knowledge and get more people involved. This year, we began to tackle tech debt together. Each quarter, we discuss ideas and decide what to work on.&lt;/p&gt;

&lt;p&gt;To show that every one of you has the power to make change, first, I will cover 5 techniques for refactoring. These are accessible and can be used on a daily basis. Next are codemods, something that's more advanced and takes time to do. Finally, we will think about how our interpersonal skills affect how we collaborate.&lt;/p&gt;

&lt;h3&gt;
  
  
  4a. Refactors
&lt;/h3&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F40.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F40.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is this book on refactoring that I hate. It gives us 70 techniques and the examples are academic, so I could never tell which are actually important.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F41.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F41.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I claim, we just need 5 to survive: Write tests, &lt;a href="https://crunchingnumbers.live/2020/08/08/3-refactoring-techniques" rel="noopener noreferrer"&gt;rename things, make early exits, extract functions&lt;/a&gt;, and remove dead code.&lt;/p&gt;

&lt;h4&gt;
  
  
  Write tests
&lt;/h4&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F42.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F42.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your project doesn't have tests, you can use &lt;a href="https://cli.emberjs.com/release/advanced-use/cli-commands-reference" rel="noopener noreferrer"&gt;Ember CLI&lt;/a&gt; to write the simplest test, a tautology (&lt;code&gt;true&lt;/code&gt; is equal to &lt;code&gt;true&lt;/code&gt;). For example, you render a component and write &lt;code&gt;assert.ok(true)&lt;/code&gt;, or you look up a service and assert that it is truthy.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F43.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F43.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even these placeholder tests provide two valuable information: the minimum data needed to initialize your object, and a guarantee that the object won't cause issues when you use it.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F44.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F44.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can learn more about &lt;a href="https://crunchingnumbers.live/2019/10/11/write-tests-like-a-mathematician-part-3" rel="noopener noreferrer"&gt;&lt;em&gt;how&lt;/em&gt; to write tests&lt;/a&gt; from &lt;a href="https://www.youtube.com/watch?v=uTjSHpJWQAY" rel="noopener noreferrer"&gt;my talk at EmberFest 2019&lt;/a&gt;. The lessons from back then still apply today.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rename things
&lt;/h4&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F45.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F45.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you don't understand what a variable, condition, or function does, there's a chance that other people won't either. Once you understand the code better, give names that are descriptive.&lt;/p&gt;

&lt;h4&gt;
  
  
  Make early exits
&lt;/h4&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F46.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F46.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nested conditions (in general, indentations to the right) are a recipe for disaster. They encourage us to keep nesting to handle new exceptions.&lt;/p&gt;

&lt;p&gt;You can fix this by making early exits. If there is code that happens when a condition is true, instead, you exit immediately when false, using &lt;code&gt;return&lt;/code&gt;, &lt;code&gt;break&lt;/code&gt;, or &lt;code&gt;continue&lt;/code&gt;. Early exits help us simplify logic and move code to the left.&lt;/p&gt;

&lt;h4&gt;
  
  
  Extract functions
&lt;/h4&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F47.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F47.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At times, you will find a function that has many lines of code, but is actually performing a few key steps in sequence. If so, create a function for each key step and give a name that describes the step.&lt;/p&gt;

&lt;p&gt;This process of breaking a large function into smaller ones is called extraction. Once you extract functions, you have the option to move them to app/utils (“utilities”) and write unit tests.&lt;/p&gt;

&lt;h4&gt;
  
  
  Remove dead code
&lt;/h4&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F48.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F48.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, if you see code that isn't used, delete it. By removing code, you can simplify assumptions, remove dependencies, and allow further refactors.&lt;/p&gt;

&lt;p&gt;To find dead code, you can use &lt;code&gt;git grep&lt;/code&gt; or your code editor's Find tool. For searches to be accurate, though, your code has to be written well (a Catch-22) and, ideally, be statically analyzable. It's hard to match names that are dynamically generated.&lt;/p&gt;

&lt;h3&gt;
  
  
  4b. Codemods
&lt;/h3&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F49.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F49.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want to update many files to follow a new format, you might ask, should I write a codemod, a program to update the files for me? It depends.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F50.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F50.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you write a codemod, you pay the costs upfront. You have to first create and configure a package, write code, then write tests to test that code. This can take days or weeks. But once you have a codemod that is backed by tests, the returns are manifold. The codemod can update your project in a second and can be reused to help other projects migrate.&lt;/p&gt;

&lt;p&gt;Here's the crux: When your project has many variations in code because it hasn't been maintained, updating it by hand will be faster. A codemod will run into edge cases that may or may not occur in other projects, and every edge case that you handle is extra code that you have to maintain.&lt;/p&gt;

&lt;p&gt;Nonetheless, the ability to help others just might be the deciding factor for you. As a rule of thumb, consider writing a codemod if you can cover the usual 80%.&lt;/p&gt;

&lt;p&gt;To get started, I would've recommended two years ago Robert Jackson's &lt;a href="https://github.com/rwjblue/codemod-cli" rel="noopener noreferrer"&gt;codemod-cli&lt;/a&gt;, but this package is unmaintained, doesn't support TypeScript, and makes a divide-and-conquer strategy—taking small steps—hard to achieve.&lt;/p&gt;

&lt;p&gt;So I created &lt;a href="https://github.com/ijlee2/codemod-utils" rel="noopener noreferrer"&gt;@codemod-utils&lt;/a&gt;, a set of tools and conventions for writing codemods. I use it to power all of mine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;blueprint-for-v2-addon (CLARK internal)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ijlee2/ember-codemod-args-to-signature/" rel="noopener noreferrer"&gt;ember-codemod-args-to-signature&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ijlee2/ember-codemod-pod-to-octane" rel="noopener noreferrer"&gt;ember-codemod-pod-to-octane&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ijlee2/embroider-css-modules/blob/main/packages/ember-codemod-remove-ember-css-modules" rel="noopener noreferrer"&gt;ember-codemod-remove-ember-css-modules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ijlee2/ember-codemod-v1-to-v2" rel="noopener noreferrer"&gt;ember-codemod-v1-to-v2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ijlee2/embroider-css-modules/tree/main/packages/type-css-modules" rel="noopener noreferrer"&gt;type-css-modules&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And just last week, I published a &lt;a href="https://github.com/ijlee2/codemod-utils/tree/main/packages/cli" rel="noopener noreferrer"&gt;CLI&lt;/a&gt;. You can use it to create a modern project that comes with lint, test, CI, and documentation out of the box.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F52.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F52.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My hope is, we can lower the barrier enough that, if a person can write a function in Node.js, then they can start writing a codemod. I'd love to see more people writing one, given that &lt;a href="https://emberjs.com/editions" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt; is coming up, and &lt;a href="https://typed-ember.gitbook.io/glint" rel="noopener noreferrer"&gt;Glint&lt;/a&gt; and &lt;a href="https://github.com/ember-template-imports/ember-template-imports" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;-tag&lt;/a&gt; can use a higher adoption. Who knows? Maybe, some day, I will give a talk on codemods.&lt;/p&gt;

&lt;h3&gt;
  
  
  4c. Different perspectives
&lt;/h3&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F53.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F53.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While solving problems together, I want you to think about your interpersonal skills and how they affect how you collaborate.&lt;/p&gt;

&lt;p&gt;Each of us, with a unique background, has a different way of thinking and verbalizing how we perceive the world. These differences surface when we discuss ideas and review each other's code. The more we are competent in our interpersonal skills, the better we can appreciate the differences and appreciate one another for who they are.&lt;/p&gt;

&lt;p&gt;We say interpersonal &lt;em&gt;skills&lt;/em&gt; because they are something that we can learn by practice. I myself learned through Toastmasters, where I met many good people. Nowadays, with more information online, I recommend that you branch out and see what interests you.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F54.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F54.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Back in Germany (and this time, it's real), you can stream shows called &lt;a href="https://www.zdf.de/kultur/sags-mir" rel="noopener noreferrer"&gt;Sag's mir&lt;/a&gt;, &lt;a href="https://www.zdf.de/kultur/unter-anderen" rel="noopener noreferrer"&gt;Unter Anderen&lt;/a&gt;, and &lt;a href="https://www.zdf.de/kultur/13-fragen" rel="noopener noreferrer"&gt;13 Fragen&lt;/a&gt;, where 2-6 people with opposing ideas carry a conversation that is personal and civilized. The goal isn't to win the argument, but to listen to each other and come up with a compromise. I really like these shows because they can teach us what makes a discussion go well.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Future is now
&lt;/h2&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F55.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F55.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last but not least, I want to show you that you can move towards the future even when your project is behind. For example, you can install polyfills to start modernizing syntax, and update &lt;code&gt;ember-cli&lt;/code&gt; to the latest, independently of &lt;code&gt;ember-source&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-polyfills/ember-angle-bracket-invocation-polyfill" rel="noopener noreferrer"&gt;ember-angle-bracket-invocation-polyfill&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/ember-cached-decorator-polyfill" rel="noopener noreferrer"&gt;ember-cached-decorator-polyfill&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-polyfills/ember-functions-as-helper-polyfill" rel="noopener noreferrer"&gt;ember-functions-as-helper-polyfill&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-polyfills/ember-in-element-polyfill" rel="noopener noreferrer"&gt;ember-in-element-polyfill&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-polyfills/ember-named-blocks-polyfill" rel="noopener noreferrer"&gt;ember-named-blocks-polyfill&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-polyfills/ember-on-modifier" rel="noopener noreferrer"&gt;ember-on-modifier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ctjhoa/ember-unique-id-helper-polyfill" rel="noopener noreferrer"&gt;ember-unique-id-helper-polyfill&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you maintain a v1 addon, you can enable two &lt;a href="https://github.com/ember-cli/ember-try" rel="noopener noreferrer"&gt;ember-try&lt;/a&gt; scenarios (&lt;code&gt;embroider-safe&lt;/code&gt; and &lt;code&gt;embroider-optimized&lt;/code&gt;) so that you can discover issues early and create a plan to adopt the v2 format. You can also support &lt;a href="https://github.com/ijlee2/ember-container-query/releases/tag/3.1.0" rel="noopener noreferrer"&gt;Glint&lt;/a&gt; and &lt;a href="https://github.com/ijlee2/ember-container-query/releases/tag/3.2.0" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;-tag&lt;/a&gt; users, even before you &lt;a href="https://github.com/ijlee2/ember-codemod-v1-to-v2" rel="noopener noreferrer"&gt;migrate to v2&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F57.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F57.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This April, at CLARK, we started using a private package registry. It helped us extract linter configurations from the monorepo, so that we can reuse them in other projects and standardize how we write code. The registry also helped us set up another monorepo (with &lt;code&gt;pnpm&lt;/code&gt;), where we extracted addons, converted them to v2, introduced &lt;a href="https://github.com/ijlee2/embroider-css-modules/tree/main/packages/embroider-css-modules" rel="noopener noreferrer"&gt;embroider-css-modules&lt;/a&gt;, supported Glint and &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;-tag, and wrote test apps to make sure that we are ready for the future.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F58.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F58.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By thinking a bit outside of the box, you just might be able to solve many problems at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Closing
&lt;/h2&gt;

&lt;p&gt;In conclusion, when your project is currently hard to maintain and extend, you may feel overwhelmed and hopeless, but please—don't give up. By taking small steps and iterating on a solution, you can introduce change that will help you and others. And you don't have to do it alone. Ask your team and the community that we have for help.&lt;/p&gt;

&lt;p&gt;In 1 year, when we are back at &lt;a href="https://www.emberconf.com/" rel="noopener noreferrer"&gt;EmberConf&lt;/a&gt;, I'd love to hear how your project is doing. Maybe you'll strike a conversation with me, in the hallway or on &lt;a href="https://discord.gg/emberjs" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;, or present a talk like I did today. Until then, mach's gut. Take care.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F59.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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2023%2F07%2F59.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ember</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Powering the Together Framework</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Mon, 31 Aug 2020 17:09:53 +0000</pubDate>
      <link>https://dev.to/ijlee2/powering-the-together-framework-1ieo</link>
      <guid>https://dev.to/ijlee2/powering-the-together-framework-1ieo</guid>
      <description>&lt;p&gt;In 2020, &lt;a href="https://emberjs.com/"&gt;Ember.js&lt;/a&gt; earned an unofficial nickname—The &lt;em&gt;Together&lt;/em&gt; Framework. Since inception, Ember championed 3 things that led to this moniker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share solutions (via battery-included framework and complementary &lt;a href="https://emberobserver.com/"&gt;addons&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Adopt new technologies via &lt;a href="https://github.com/emberjs/rfcs"&gt;RFCs&lt;/a&gt; (open to all)&lt;/li&gt;
&lt;li&gt;Pave migration paths (e.g. &lt;a href="https://emberjs.com/deprecations/"&gt;deprecation warnings&lt;/a&gt;, &lt;a href="https://github.com/ember-codemods"&gt;codemods&lt;/a&gt;) so that developers who maintain older apps aren't abandoned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because developers and teams follow a set of conventions, it's particularly easy to write GitHub Actions workflows that can be &lt;strong&gt;shared among Ember apps and addons&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  My Workflow
&lt;/h3&gt;

&lt;p&gt;For the hackathon, I created workflow templates for Ember apps and addons.&lt;/p&gt;

&lt;p&gt;✅ The workflow for Ember apps has several features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leverages 5 &lt;a href="https://github.com/actions/"&gt;officially supported actions&lt;/a&gt; (all &lt;code&gt;v2&lt;/code&gt; or &lt;code&gt;v2-beta&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Lints files and dependencies&lt;/li&gt;
&lt;li&gt;Runs tests in parallel&lt;/li&gt;
&lt;li&gt;Takes &lt;a href="http://percy.io/"&gt;Percy&lt;/a&gt; snapshots in parallel&lt;/li&gt;
&lt;li&gt;Caches &lt;code&gt;node_modules&lt;/code&gt; for faster run&lt;/li&gt;
&lt;li&gt;Pre-builds test app for faster run&lt;/li&gt;
&lt;li&gt;Deploys the app (to any provider that &lt;a href="http://ember-cli-deploy.com/"&gt;ember-cli-deploy&lt;/a&gt; supports, such as &lt;a href="https://github.com/ef4/ember-cli-deploy-git"&gt;GitHub Pages&lt;/a&gt;, &lt;a href="https://github.com/exelord/ember-cli-deploy-netlify-cli"&gt;Netlify&lt;/a&gt;, and &lt;a href="https://github.com/ember-cli-deploy/ember-cli-deploy-s3"&gt;S3&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ The workflow for Ember addons has several features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leverages 5 &lt;a href="https://github.com/actions/"&gt;officially supported actions&lt;/a&gt; (all &lt;code&gt;v2&lt;/code&gt; or &lt;code&gt;v2-beta&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Lints files and dependencies&lt;/li&gt;
&lt;li&gt;Runs tests in parallel&lt;/li&gt;
&lt;li&gt;Takes &lt;a href="http://percy.io/"&gt;Percy&lt;/a&gt; snapshots in parallel&lt;/li&gt;
&lt;li&gt;Caches &lt;code&gt;node_modules&lt;/code&gt; for faster run&lt;/li&gt;
&lt;li&gt;Pre-builds test app for faster run&lt;/li&gt;
&lt;li&gt;Ensures that addon works with &lt;a href="https://github.com/ember-cli/ember-try"&gt;LTS, release, beta, and canary versions&lt;/a&gt; of Ember&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To top these off, I created &lt;a href="https://github.com/ijlee2/inspect-workflow-runs"&gt;inspect-workflow-runs&lt;/a&gt;. You can analyze past workflow runs and make a data-driven decision for calibrating &lt;code&gt;timeout-minutes&lt;/code&gt;. (The default value is &lt;a href="https://docs.github.com/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes"&gt;360 minutes&lt;/a&gt;, which can lead to accidentally running out of minutes in private repos.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Submission Category:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Maintainer Must-Haves&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Yaml File or Link to Code
&lt;/h3&gt;

&lt;p&gt;I created 4 workflow templates for the hackathon. They account for Ember addon vs. app, and yarn vs. npm.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ember addons: &lt;a href="https://gist.github.com/ijlee2/25004d700cd008ee08ecc4873c109a5b"&gt;yarn&lt;/a&gt;, &lt;a href="https://gist.github.com/ijlee2/fba20304670b8d2b71f0b81cd07ea026"&gt;npm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ember apps: &lt;a href="https://gist.github.com/ijlee2/1c864ebe96a55f239e80800829ef0bf4"&gt;yarn&lt;/a&gt;, &lt;a href="https://gist.github.com/ijlee2/6576ae82895d522ccc0695518c2a6ce7"&gt;npm&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My main entry for the hackathon is &lt;a href="https://gist.github.com/ijlee2/25004d700cd008ee08ecc4873c109a5b"&gt;Ember addons - yarn&lt;/a&gt;. Addons like &lt;code&gt;ember-container-query&lt;/code&gt; are the open-sourced npm packages that bring the Ember community together to develop, share, and support. 🧡&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Az_zNVNB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/08/cover_image_github_actions.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Az_zNVNB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/08/cover_image_github_actions.png" alt="A workflow run for Ember Container Query shows 20 jobs that completed in 3 minutes." width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
A workflow run for &lt;code&gt;ember-container-query&lt;/code&gt; shows 20 jobs that completed in 3 minutes. Thanks to CI and thorough testing, the addon has had 0 bug reports.



&lt;h3&gt;
  
  
  Additional Resources / Info
&lt;/h3&gt;

&lt;p&gt;To learn more about writing GitHub Actions workflows for Ember projects, I recommend my blog posts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://crunchingnumbers.live/2020/03/17/ci-with-github-actions-for-ember-apps/"&gt;CI with GitHub Actions for Ember Apps: Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://crunchingnumbers.live/2020/08/31/ci-with-github-actions-for-ember-apps-part-2/"&gt;CI with GitHub Actions for Ember Apps: Part 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workflow templates that I built now power a few open source projects for the Ember community:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ijlee2/ember-container-query/blob/main/.github/workflows/ci.yml"&gt;ember-container-query&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-learn/ember-octane-vs-classic-cheat-sheet/blob/master/.github/workflows/ci-cd.yml"&gt;ember-octane-vs-classic-cheat-sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dknutsen/ember-themed/blob/master/.github/workflows/ci.yml"&gt;ember-themed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-learn/ember-times-tools/blob/main/.github/workflows/ci.yml"&gt;ember-times-tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-learn/whats-new-in-emberland/blob/master/.github/workflows/ci.yml"&gt;whats-new-in-emberland&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While I created the workflow templates on my own, my knowledge and experience with GitHub Actions stand upon those of amazing developers in the Ember community. I'd like to recognize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/dknutsen"&gt;Dan Knutsen&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kategengler"&gt;Katie Gengler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jenweber"&gt;Jen Weber&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/buschtoens"&gt;Jan Buschtöns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dfreeman"&gt;Dan Freeman&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://github.com/NullVoxPopuli"&gt;Preston Sego&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>actionshackathon</category>
      <category>ember</category>
      <category>javascript</category>
      <category>cicd</category>
    </item>
    <item>
      <title>CI with GitHub Actions for Ember Apps: Part 2</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Mon, 31 Aug 2020 16:05:49 +0000</pubDate>
      <link>https://dev.to/ijlee2/ci-with-github-actions-for-ember-apps-part-2-30ph</link>
      <guid>https://dev.to/ijlee2/ci-with-github-actions-for-ember-apps-part-2-30ph</guid>
      <description>&lt;p&gt;2020 has been a tough, frail year. Last week, I joined many people who were laid off. Still I'm grateful for the good things that came out like &lt;a href="https://www.youtube.com/watch?v=BE7vBk_zLA4&amp;amp;list=PL8tkzXKlhGxn8vedFOKUngfcdnz6MV40M"&gt;Dreamland&lt;/a&gt; and &lt;a href="https://crunchingnumbers.live/2020/03/17/ci-with-github-actions-for-ember-apps/"&gt;CI with GitHub Actions for Ember Apps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With GitHub Actions, I cut down CI runtimes for work projects to 3-4 minutes (with lower variance and more tests since March). I also noticed more and more Ember projects switching to GitHub Actions so I felt like a pioneer.&lt;/p&gt;

&lt;p&gt;Today, I want to patch my original post and cover 3 new topics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to migrate to v2 actions&lt;/li&gt;
&lt;li&gt;How to lower runtime cost&lt;/li&gt;
&lt;li&gt;How to continuously deploy (with &lt;a href="http://ember-cli-deploy.com/"&gt;ember-cli-deploy&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will assume that you read &lt;a href="https://crunchingnumbers.live/2020/03/17/ci-with-github-actions-for-ember-apps/"&gt;Part 1&lt;/a&gt; and are familiar with my workflow therein. Towards the end, you can find new workflow templates for Ember addons and apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. How to Migrate to v2 Actions
&lt;/h2&gt;

&lt;p&gt;In Part 1, you met 3 actions that are officially supported by GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/actions/checkout"&gt;actions/checkout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/actions/setup-node"&gt;actions/setup-node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/actions/cache"&gt;actions/cache&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check out the README to find new features and improvements in v2. If you followed my workflow, you should be able to use v2 without a problem.&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint&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;Lint files and dependencies&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;Check out a copy of the repo&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use Node.js ${{ env.NODE_VERSION }}&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-node@v2-beta&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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.NODE_VERSION }}&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;Get Yarn cache path&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn-cache-dir-path&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "::set-output name=dir::$(yarn cache dir)"&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;Cache Yarn cache and node_modules&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cache-dependencies&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/cache@v2&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;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ steps.yarn-cache-dir-path.outputs.dir }}&lt;/span&gt;
            &lt;span class="s"&gt;node_modules&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;${{ runner.os }}-${{ env.NODE_VERSION }}-${{ hashFiles('**/yarn.lock') }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-${{ env.NODE_VERSION }}-&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 dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install --frozen-lockfile&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.cache-dependencies.outputs.cache-hit != 'true'&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;Lint&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn lint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;actions/cache@v2&lt;/code&gt; allows &lt;strong&gt;caching multiple things in one step&lt;/strong&gt;. As a result, the cache retrieval step (line 29) is simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. How to Lower Runtime Cost
&lt;/h2&gt;

&lt;p&gt;I neglected to warn cost last time. For private repos, where production apps are likely stored, GitHub Actions charges you by the minute. 2020 taught me that money doesn't grow on trees.&lt;/p&gt;

&lt;p&gt;You can control 3 things to lower cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set operating system&lt;/li&gt;
&lt;li&gt;Lower job runtime&lt;/li&gt;
&lt;li&gt;Lower &lt;code&gt;timeout-minutes&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if your repo is public and immune from charge, I recommend the last 2 practices to lower the overall runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  a. Set Operating System
&lt;/h3&gt;

&lt;p&gt;In Part 1, I suggested that you can use &lt;code&gt;matrix&lt;/code&gt; to test the app against vari ous operating systems. I must redact because jobs that run on Windows and Mac &lt;a href="https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions#about-billing-for-github-actions"&gt;cost 2 and 10 times as much&lt;/a&gt; as those on Linux. The rate difference also applies to storage used by GitHub Actions artifacts, which we will soon leverage.&lt;/p&gt;

&lt;p&gt;Unless you have a compelling business requirement, &lt;strong&gt;run jobs on Linux only&lt;/strong&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint&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;Lint files and dependencies&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  b. Lower Job Runtime
&lt;/h3&gt;

&lt;p&gt;When a workflow runs, you pay for the &lt;em&gt;sum&lt;/em&gt; of all job runtimes. You don't pay for the workflow runtime (except in the sense of feedback loop).&lt;/p&gt;

&lt;p&gt;Our workflow has 1 lint and 4 test jobs. Suppose these jobs took 1:40, 3:20, 4:00, 4:30, and 3:40 minutes to run. In total, the jobs took,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1:40 + 3:20 + 4:00 + 4:30 + 3:40 = 17.10 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We round up that number, then multiply by the per-minute rate ($0.008/min for Linux) to arrive at the cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;18 minutes × $0.008/minute = $0.144
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;14.4 cents seem trivial until you realize that your team can make hundreds or thousands of commits each month. (See &lt;a href="https://crunchingnumbers.live/2020/03/17/ci-with-github-actions-for-ember-apps/"&gt;Part 1, Section 1c&lt;/a&gt; to learn more about configuring &lt;code&gt;on&lt;/code&gt; correctly.)&lt;/p&gt;

&lt;p&gt;There's a silver lining for Ember developers. The predominant jobs in our workflow are tests. A test job takes a while to run because it needs to build the app. What if you can &lt;strong&gt;build the test app once&lt;/strong&gt; and pass it to each job—a form of caching?&lt;/p&gt;

&lt;p&gt;Since &lt;a href="https://github.com/ember-cli/ember-cli/pull/4874"&gt;2015&lt;/a&gt;, &lt;code&gt;ember test&lt;/code&gt; has let you pass &lt;code&gt;--path&lt;/code&gt; to tell there's a pre-built &lt;code&gt;dist&lt;/code&gt; folder somewhere. You can set the location thanks to 2 officially-supported actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/actions/upload-artifact"&gt;actions/upload-artifact&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/actions/download-artifact"&gt;actions/download-artifact&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even better, the &lt;code&gt;--path&lt;/code&gt; flag works with &lt;a href="https://ember-cli.com/ember-exam/docs/randomization-iterator"&gt;ember-exam&lt;/a&gt; and &lt;a href="https://docs.percy.io/docs/ember"&gt;@percy/ember&lt;/a&gt;. Here is a simplified update:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-app&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 app for testing&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;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 app&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn build:test&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;Upload app&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/upload-artifact@v2&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&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;dist&lt;/span&gt;

  &lt;span class="na"&gt;test-app&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;Test app&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;build-app&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;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;partition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;4&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download app&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/download-artifact@v2&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&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;dist&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;Test&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;percy/exec-action@v0.3.0&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;custom-command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test --partition=${{ matrix.partition }} --path=dist&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;PERCY_PARALLEL_NONCE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.PERCY_PARALLEL_NONCE }}&lt;/span&gt;
          &lt;span class="na"&gt;PERCY_PARALLEL_TOTAL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.PERCY_PARALLEL_TOTAL }}&lt;/span&gt;
          &lt;span class="na"&gt;PERCY_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PERCY_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the use of &lt;code&gt;needs&lt;/code&gt; (line 17) to indicate a dependency among jobs. All &lt;code&gt;test-app&lt;/code&gt; jobs won't start until the &lt;code&gt;build-app&lt;/code&gt; job has finished.&lt;/p&gt;

&lt;p&gt;Although the workflow performs 1 additional job, the total runtime can be less because tests can finish sooner. When I introduced this change at work, I saw a 33% decrease (6-8 minutes) in billable minutes. That's 50% more runs for the same cost.&lt;/p&gt;

&lt;p&gt;The last thing to note is, we must build the Ember app in the test environment (line 7). The default &lt;code&gt;build&lt;/code&gt; script makes a production build so I wrote &lt;code&gt;build:test&lt;/code&gt; to make a test build. If you pass a production build, the tests won't run and will eventually time out (in CI and locally):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;message: &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  Error: Browser failed to connect within 120s. testem.js not loaded?
  Stderr: 
    &lt;span class="o"&gt;[&lt;/span&gt;0824/133551.179006:ERROR:xattr.cc&lt;span class="o"&gt;(&lt;/span&gt;63&lt;span class="o"&gt;)]&lt;/span&gt; setxattr org.chromium.crashpad.database.initialized on file /var/folders/2z/93zyyhx13rs879qr8rzyxrb40000gn/T/: Operation not permitted &lt;span class="o"&gt;(&lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt;0824/133551.180908:ERROR:file_io.cc&lt;span class="o"&gt;(&lt;/span&gt;89&lt;span class="o"&gt;)]&lt;/span&gt; ReadExactly: expected 8, observed 0
    &lt;span class="o"&gt;[&lt;/span&gt;0824/133551.182193:ERROR:xattr.cc&lt;span class="o"&gt;(&lt;/span&gt;63&lt;span class="o"&gt;)]&lt;/span&gt; setxattr org.chromium.crashpad.database.initialized on file /var/folders/2z/93zyyhx13rs879qr8rzyxrb40000gn/T/: Operation not permitted &lt;span class="o"&gt;(&lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;

  DevTools listening on ws://127.0.0.1:63192/devtools/browser/9ffa155c-99b3-4f7f-a53e-b23cff1bf743
    &lt;span class="o"&gt;[&lt;/span&gt;0824/133551.670401:ERROR:command_buffer_proxy_impl.cc&lt;span class="o"&gt;(&lt;/span&gt;122&lt;span class="o"&gt;)]&lt;/span&gt; ContextResult::kTransientFailure: Failed to send GpuChannelMsg_CreateCommandBuffer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  c. Lower timeout-minutes
&lt;/h3&gt;

&lt;p&gt;GitHub Actions doesn't emphasize the need to set &lt;code&gt;timeout-minutes&lt;/code&gt;. It's how long a job can run (stall) before GitHub Actions cancels the workflow. You're still charged for the run so it's important to know that the &lt;a href="https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepstimeout-minutes"&gt;default timeout is 360 minutes&lt;/a&gt; (!!).&lt;/p&gt;

&lt;p&gt;In short, if a workflow is to fail, let it &lt;strong&gt;fail fast&lt;/strong&gt;. Make sure to set a low &lt;code&gt;timeout-minutes&lt;/code&gt; for each job:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-app&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 app for testing&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;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;

  &lt;span class="na"&gt;lint&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;Lint files and dependencies&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;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;

  &lt;span class="na"&gt;test-app&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;Test app&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;build-app&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;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good initial value is how long build, lint, and test take locally, plus some wiggle room. Over time, however, you will want to observe runtimes and calibrate timeout.&lt;/p&gt;

&lt;p&gt;To help you make a data-driven decision, I created &lt;a href="https://github.com/ijlee2/inspect-workflow-runs"&gt;inspect-workflow-runs&lt;/a&gt;. The script finds past runs and recommends timeout based on 95% confidence interval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timeout-minutes ≈ x̅ + 2s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Speaking of failing fast, GitHub Actions lets you &lt;a href="https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategyfail-fast"&gt;cancel in-progress jobs&lt;/a&gt; if any &lt;code&gt;matrix&lt;/code&gt; job fails. This may be useful if you use &lt;a href="https://github.com/ember-cli/ember-try"&gt;ember-try&lt;/a&gt; or &lt;a href="https://crunchingnumbers.live/2020/06/07/container-queries-cross-resolution-testing/"&gt;cross-resolution testing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. How to Continuously Deploy
&lt;/h2&gt;

&lt;p&gt;In Part 1, I mentioned auto-deployment with &lt;a href="https://www.heroku.com/"&gt;Heroku&lt;/a&gt;. Since then, I got to deploy Ember apps to &lt;a href="https://pages.github.com/"&gt;GitHub Pages&lt;/a&gt; and &lt;a href="https://www.netlify.com/"&gt;Netlify&lt;/a&gt; thanks to open source work. I became curious about deploying apps from a GitHub Actions workflow.&lt;/p&gt;

&lt;p&gt;The Ember community has a dedicated addon called &lt;a href="http://ember-cli-deploy.com/"&gt;ember-cli-deploy&lt;/a&gt;. It has several plugins so that you can customize the deployment pipeline. Afterwards, you call &lt;code&gt;ember deploy production&lt;/code&gt;, which you can certainly do from a workflow. The hard parts may be building the pipeline and passing your credentials.&lt;/p&gt;

&lt;p&gt;As a concrete example, we'll look at &lt;strong&gt;deploying to GitHub Pages&lt;/strong&gt; with the plugin &lt;a href="https://github.com/ef4/ember-cli-deploy-git"&gt;ember-cli-deploy-git&lt;/a&gt;. I'll cover a basic setup and 2 ways to pass credentials. You can review the &lt;a href="https://github.com/ember-learn/ember-octane-vs-classic-cheat-sheet/pull/58"&gt;changes to ember-octane-vs-classic-cheat-sheet&lt;/a&gt; to see an implementation.&lt;/p&gt;

&lt;p&gt;As for deploying to Netlify, although there is a &lt;a href="https://github.com/exelord/ember-cli-deploy-netlify-cli"&gt;plugin&lt;/a&gt;, I'd use the standalone &lt;a href="https://github.com/shipshapecode/ember-cli-netlify"&gt;ember-cli-netlify&lt;/a&gt; for simple static sites. Netlify can listen to a push to the default branch (similarly to Heroku) so we just need something to handle routing. You can review the &lt;a href="https://github.com/ijlee2/ember-container-query/pull/54/files"&gt;changes to ember-container-query&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  a. Setup
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1
&lt;/h4&gt;

&lt;p&gt;We'll deploy the app to the &lt;code&gt;gh-pages&lt;/code&gt; branch. After we create the branch,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;--orphan&lt;/span&gt; gh-pages
git commit &lt;span class="nt"&gt;--allow-empty&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s1"&gt;'Created gh-pages branch for deployment'&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin gh-pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we ask GitHub Pages to build the site from &lt;code&gt;gh-pages&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SMTAlCZq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/08/section_3a_github_pages.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SMTAlCZq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/08/section_3a_github_pages.png" alt="Select the deploy branch in Settings - Options - GitHub Pages." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
Select the deploy branch in Options - GitHub Pages.



&lt;h4&gt;
  
  
  Step 2
&lt;/h4&gt;

&lt;p&gt;Let's come back to the default branch. We need to install a few addons:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ember &lt;span class="nb"&gt;install &lt;/span&gt;ember-cli-deploy ember-cli-deploy-build ember-cli-deploy-git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command will create &lt;code&gt;config/deploy.js&lt;/code&gt;. For now, we can leave this file alone. We'll look at it later in the context of setting credentials.&lt;/p&gt;

&lt;p&gt;Do update &lt;code&gt;config/environment.js&lt;/code&gt; so that GitHub Pages understands the routing of the app:&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;// config/environment.js&lt;/span&gt;

&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&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="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ENV&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="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;environment&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&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;ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rootURL&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-repo-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locationType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hash&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ENV&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;h4&gt;
  
  
  Step 3
&lt;/h4&gt;

&lt;p&gt;Finally, create a &lt;code&gt;deploy&lt;/code&gt; script in &lt;code&gt;package.json&lt;/code&gt;.&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;"scripts"&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;"deploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ember deploy production"&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;p&gt;Now, we can run &lt;code&gt;yarn deploy&lt;/code&gt; to deploy the app from the local machine. Let's look at how to deploy from the workflow next.&lt;/p&gt;

&lt;h3&gt;
  
  
  b. Create a Deploy Key
&lt;/h3&gt;

&lt;p&gt;We can't simply add a step that runs &lt;code&gt;yarn deploy&lt;/code&gt; because GitHub Actions will ask for authentication. When everything is meant to be automated, how do you authenticate?&lt;/p&gt;

&lt;p&gt;One solution is to check the public key against a private. We can store the latter as a secret environment variable for the workflow, much like we had with the Percy token. The authentication details are hidden thanks to the &lt;a href="https://github.com/dfreeman/ember-cli-deploy-git-ci"&gt;ember-cli-deploy-git-ci&lt;/a&gt; plugin.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1
&lt;/h4&gt;

&lt;p&gt;Install the plugin and generate a key pair.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ember &lt;span class="nb"&gt;install &lt;/span&gt;ember-cli-deploy-git-ci

ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; rsa &lt;span class="nt"&gt;-b&lt;/span&gt; 4096 &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; deploy_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The public key (&lt;code&gt;deploy_key.pub&lt;/code&gt;) belongs to &lt;strong&gt;Deploy keys&lt;/strong&gt; in the repo's Settings page. The private key (&lt;code&gt;deploy_key&lt;/code&gt;) goes to &lt;strong&gt;Secrets&lt;/strong&gt; and becomes an environment variable called &lt;code&gt;DEPLOY_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AkfnnJNl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/08/section_3b_public_key.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AkfnnJNl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/08/section_3b_public_key.png" alt="The public key goes to Settings - Deploy keys." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
The public key goes to Deploy keys.



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UgYSYhAv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/08/section_3b_private_key.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UgYSYhAv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/08/section_3b_private_key.png" alt="The private key goes to Settings - Secrets." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
The private key goes to Secrets.



&lt;p&gt;After saving these keys in GitHub, please delete &lt;code&gt;deploy_key.pub&lt;/code&gt; and &lt;code&gt;deploy_key&lt;/code&gt; so that they won't be committed to the repo.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2
&lt;/h4&gt;

&lt;p&gt;We update &lt;code&gt;config/deploy.js&lt;/code&gt; to indicate the presence of an SSH key:&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;// config/deploy.js&lt;/span&gt;

&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&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="nx"&gt;deployTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ENV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;

    &lt;span class="na"&gt;git&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;git@github.com:your-username/your-repo-name.git&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="s1"&gt;git-ci&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="na"&gt;enabled&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;deployKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SECRET_KEY&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ENV&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;h4&gt;
  
  
  Step 3
&lt;/h4&gt;

&lt;p&gt;Finally, we add a deploy job to the workflow. We can use &lt;code&gt;needs&lt;/code&gt; and &lt;code&gt;if&lt;/code&gt; to describe when the app should be deployed (e.g. when there is a push to the &lt;code&gt;main&lt;/code&gt; branch).&lt;/p&gt;

&lt;p&gt;Here's a simplified update:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy-app&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;Deploy app&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;test-app&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;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'push' &amp;amp;amp;&amp;amp;amp; github.ref == 'refs/heads/main'&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;Check out a copy of the repo&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;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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn deploy&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DEPLOY_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_KEY }}&amp;lt;/pre&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  c. Reuse Auth Token
&lt;/h3&gt;

&lt;p&gt;Thanks to &lt;code&gt;actions/checkout@v2&lt;/code&gt;, there's an easier way to authenticate—one that doesn't require &lt;code&gt;ember-cli-deploy-git-ci&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While a job runs, the checkout action persists the auth token in the local git config. As a result, we can set GitHub Actions as the user who wants to deploy the app, but pass our auth token instead:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy-app&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;Deploy app&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;test-app&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;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'push' &amp;amp;amp;&amp;amp;amp; github.ref == 'refs/heads/main'&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;Check out a copy of the repo&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Git user&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;# Set up a Git user for committing&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.name "GitHub Actions"&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.email "actions@users.noreply.github.com"&lt;/span&gt;

          &lt;span class="s"&gt;# Copy the Git Auth from the local config&lt;/span&gt;
          &lt;span class="s"&gt;git config --global "http.https://github.com/.extraheader" \&lt;/span&gt;
            &lt;span class="s"&gt;"$(git config --local --get http.https://github.com/.extraheader)"&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;Deploy&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn deploy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last but not least, we provide an HTTPS URL in &lt;code&gt;config/deploy.js&lt;/code&gt;.&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;// config/deploy.js&lt;/span&gt;

&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&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="nx"&gt;deployTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ENV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;

    &lt;span class="na"&gt;git&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;repo&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://github.com/your-username/your-repo-name.git&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="p"&gt;...&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ENV&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;
  
  
  4. Conclusion
&lt;/h2&gt;

&lt;p&gt;Thanks to shared solutions in Ember (the &lt;em&gt;Together&lt;/em&gt; Framework) and new features in v2 actions, we saw that CI/CD with GitHub Actions continues to work well for Ember apps and addons.&lt;/p&gt;

&lt;p&gt;We should watch out for long-running jobs because they cost money (even for public repos in the forms of feedback loop and developer's time). In Part 1, we learned to save time by running tests in parallel and caching &lt;code&gt;node_modules&lt;/code&gt;. In Part 2, by building the test app once and employing a fail-fast strategy.&lt;/p&gt;

&lt;p&gt;If you haven't yet, I hope that you will give GitHub Actions a try and share what you learned. I look forward to discover more ways to optimize and enhance workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Notes
&lt;/h2&gt;

&lt;p&gt;A few sections in Part 2 were possible thanks to the Ember community:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dknutsen"&gt;Dan Knutsen&lt;/a&gt; showed me how to pre-build the app for tests.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kategengler"&gt;Katie Gengler&lt;/a&gt; created the pre-build example in &lt;a href="https://github.com/emberjs/ember.js/blob/master/.github/workflows/ci.yml#L36-L63"&gt;ember.js&lt;/a&gt; and directed Dan to it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/jenweber"&gt;Jen Weber&lt;/a&gt; walked me through how to use &lt;code&gt;ember-cli-deploy-git&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/buschtoens"&gt;Jan Buschtöns&lt;/a&gt; and &lt;a href="https://github.com/dfreeman"&gt;Dan Freeman&lt;/a&gt; found a way to continuously deploy to GitHub Pages without &lt;code&gt;ember-cli-deploy-git-ci&lt;/code&gt;. They shared their solution on &lt;a href="https://discordapp.com/channels/480462759797063690/485881813370273792/745992323485401150"&gt;Discord&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Katie kindly informed me that it's also possible to pre-build an addon's demo app for every &lt;code&gt;ember-try&lt;/code&gt; scenario. (I wanted to test an addon at different window sizes.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ember try:one scenario-name &lt;span class="nt"&gt;---&lt;/span&gt; ember build &lt;span class="nt"&gt;--environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Katie recommends caching &lt;code&gt;dist&lt;/code&gt; (with a unique hash based on Node version, scenario name, and lockfile) over uploading it as an artifact. This is to avoid the possibility of passing the wrong &lt;code&gt;dist&lt;/code&gt; to a scenario.&lt;/p&gt;

&lt;p&gt;I posted new workflow templates on GitHub Gist.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ember addons: &lt;a href="https://gist.github.com/ijlee2/25004d700cd008ee08ecc4873c109a5b"&gt;yarn&lt;/a&gt;, &lt;a href="https://gist.github.com/ijlee2/fba20304670b8d2b71f0b81cd07ea026"&gt;npm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ember apps: &lt;a href="https://gist.github.com/ijlee2/1c864ebe96a55f239e80800829ef0bf4"&gt;yarn&lt;/a&gt;, &lt;a href="https://gist.github.com/ijlee2/6576ae82895d522ccc0695518c2a6ce7"&gt;npm&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are interested in cross-resolution testing, I recommend studying the &lt;a href="https://github.com/ijlee2/ember-container-query/blob/main/.github/workflows/ci.yml"&gt;workflow for ember-container-query&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ember</category>
      <category>javascript</category>
      <category>ci</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>3 Refactoring Techniques</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Sun, 09 Aug 2020 11:07:15 +0000</pubDate>
      <link>https://dev.to/ijlee2/3-refactoring-techniques-2cai</link>
      <guid>https://dev.to/ijlee2/3-refactoring-techniques-2cai</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This mess was yours. Now your mess is mine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;- Vance Joy&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;a href="https://hacktoberfest.digitalocean.com/"&gt;Hacktoberfest&lt;/a&gt; is coming up. If you're new to open source contribution and unsure how to help, may I suggest refactoring code? You can provide a fresh perspective to unclear code and discover ways to leave it better than you found.&lt;/p&gt;

&lt;p&gt;There are 3 refactoring techniques that I often practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rename things&lt;/li&gt;
&lt;li&gt;Remove nests&lt;/li&gt;
&lt;li&gt;Extract functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Knowing how to apply just these 3 can get you far. I'll explain what they mean and how I used them (or should have used them) in projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Rename Things
&lt;/h2&gt;

&lt;p&gt;The goal of this technique is to &lt;strong&gt;help people communicate through code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I remember this story from my college professor. She once had inherited code in which variables had been named after baseball players. Why? I can only imagine spite from an unhappy programmer.&lt;/p&gt;

&lt;p&gt;If you didn't understand right away what a variable, conditional, or function does, then there's a chance that someone else won't either. Once you do understand what it does and how it interplays with other code, please give it a better name.&lt;/p&gt;

&lt;h3&gt;
  
  
  a. Variables
&lt;/h3&gt;

&lt;p&gt;A variable name, done right, explains purpose. In general, you will want to prefer fully-spelled words over truncated ones. This removes ambiguity and allows guessing when searching code by text.&lt;/p&gt;

&lt;p&gt;Here's a change that I made to &lt;a href="https://github.com/ember-learn/whats-new-in-emberland"&gt;whats-new-in-emberland&lt;/a&gt;, an app that helps &lt;a href="https://blog.emberjs.com/tags/newsletter.html"&gt;The Ember Times&lt;/a&gt; newsletter find who contributed to Ember repos.&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;// Before&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;conListUniq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;contributorsList&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Had I not mentioned to you that the app finds contributors, I think you would've had a hard time deciphering what "conList" in &lt;code&gt;conListUniq&lt;/code&gt; means.&lt;/p&gt;

&lt;p&gt;A variable name can also explain type. For example, you can begin the name with &lt;code&gt;is&lt;/code&gt; or &lt;code&gt;can&lt;/code&gt; to indicate a boolean and pluralize the name to denote an array. Plural name comes in handy when you iterate over the array. You can use the singular noun for the array element.&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;filterMerged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pullRequests&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;pullRequests&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="nx"&gt;pullRequest&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;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isMadeByUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isMergedThisWeek&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pullRequest&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;isMadeByUser&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isMergedThisWeek&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the naming conventions in the variables &lt;code&gt;pullRequests&lt;/code&gt;, &lt;code&gt;pullRequest&lt;/code&gt;, &lt;code&gt;isMadeByUser&lt;/code&gt;, and &lt;code&gt;isMergedThisWeek&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  b. Conditionals
&lt;/h3&gt;

&lt;p&gt;A conditional statement, since it is made up of dynamic values and language-specific syntax, can be tough to digest at once. This is more true for a compound conditional—two or more statements joined by the &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; or &lt;code&gt;||&lt;/code&gt; operator.&lt;/p&gt;

&lt;p&gt;To maintain conditionals, try creating temporary variables with a clear name. In general, each statement in a compound conditional should get its own variable. When you read the code aloud, it will sound almost natural.&lt;/p&gt;

&lt;p&gt;Some time ago, I added a feature to &lt;a href="https://github.com/ember-codemods/ember-component-template-colocation-migrator"&gt;ember-component-template-colocation-migrator&lt;/a&gt;. It runs in command line so I needed to support a couple of flags.&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;argv&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yargs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Allow passing the flag, -fs (flat) or -ns (nested), to specify component structure&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;changeToFlatStructure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;changeToNestedStructure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;structure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flat&lt;/span&gt;&lt;span class="dl"&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;changeToFlatStructure&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;structure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flat&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="k"&gt;else&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;changeToNestedStructure&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;structure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nested&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;Creating temporary variables has the benefit of abstraction. If we later decide to use a different library to handle flags, only lines 1-5 would change. The branching logic for &lt;code&gt;structure&lt;/code&gt; can remain the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  c. Functions
&lt;/h3&gt;

&lt;p&gt;In Section 3, we will look at how functions play a critical role in refactoring.&lt;/p&gt;

&lt;p&gt;As for naming, I encourage you to start a function's name with a verb. I like to use &lt;code&gt;get&lt;/code&gt; or &lt;code&gt;find&lt;/code&gt; to indicate a function that retrieves data, and &lt;code&gt;set&lt;/code&gt; or &lt;code&gt;update&lt;/code&gt; to denote one that changes data.&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;action&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;getContributors&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchRequests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mergedPRs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pullRequest&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pullRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetchRequests&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identifyUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sortUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updateContributorsList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&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;Notice the use of verbs in &lt;code&gt;getContributors&lt;/code&gt;, &lt;code&gt;identifyUsers&lt;/code&gt;, &lt;code&gt;sortUsers&lt;/code&gt;, and &lt;code&gt;updateContributorsList&lt;/code&gt;. Although you don't see their implementation, you may be able to guess what each is supposed to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Remove Nests
&lt;/h2&gt;

&lt;p&gt;Removing nests is about &lt;strong&gt;flattening code structure&lt;/strong&gt;. By removing indentations that are unnecessary, the ones that remain can clearly show groups of related code.&lt;/p&gt;

&lt;p&gt;Since code indentation is a bit of styling choice, you might wonder why removing nests matters. We'll look at nested conditionals and promises to see their drawbacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  a. Nested Conditionals
&lt;/h3&gt;

&lt;p&gt;Over time, a nested if statement can turn into mess. Business logic changes constantly. Pressured by time, we may add exceptions to allow new logic rather than refactor code in order to find a holistic solution.&lt;/p&gt;

&lt;p&gt;The best fictitious example comes from Sandi Metz's 2014 RailsConf talk, &lt;a href="https://www.youtube.com/watch?v=8bZh5LMaSmE"&gt;All the Little Things&lt;/a&gt;. Sandi talks of the &lt;a href="https://github.com/NotMyself/GildedRose"&gt;Gilded Rose&lt;/a&gt; problem. Given this code,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tick&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'Aged Brie'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'Backstage passes to a TAFKAL80ETC concert'&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'Sulfuras, Hand of Ragnaros'&lt;/span&gt;
        &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
      &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'Backstage passes to a TAFKAL80ETC concert'&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
            &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
            &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'Sulfuras, Hand of Ragnaros'&lt;/span&gt;
    &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'Aged Brie'&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'Backstage passes to a TAFKAL80ETC concert'&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'Sulfuras, Hand of Ragnaros'&lt;/span&gt;
            &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
        &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you are to update &lt;code&gt;tick&lt;/code&gt; to handle just 1 more feature and ensure that all tests continue to pass. Where do you even begin?&lt;/p&gt;

&lt;p&gt;The key to refactoring &lt;code&gt;tick&lt;/code&gt; is to &lt;strong&gt;make early exits&lt;/strong&gt;, also called guard clauses. If you see code that can happen only when a condition is true, you leave immediately where you are (using &lt;code&gt;return&lt;/code&gt;, &lt;code&gt;break&lt;/code&gt;, or &lt;code&gt;continue&lt;/code&gt;) if it evaluates to false.&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;// Before&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;myExample&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;condition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* Complex code omitted */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;myExample&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;condition&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="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="cm"&gt;/* Complex code omitted */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that we removed 1 indentation level from the complex code. Imagine you are able to make a few early exits. With each non-exit, the complex code can become simpler and allow other refactors. Moreover, by reading the series of &lt;code&gt;if&lt;/code&gt; statements from top to bottom, you know exactly when the next code runs.&lt;/p&gt;

&lt;p&gt;Through a series of small refactors, Sandi arrives at the following code. I bet that you can more easily understand and change this code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tick&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'normal'&lt;/span&gt;
    &lt;span class="n"&gt;normal_tick&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'Aged Brie'&lt;/span&gt;
    &lt;span class="n"&gt;brie_tick&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'Sulfuras, Hand of Ragnaros'&lt;/span&gt;
    &lt;span class="n"&gt;sulfuras_tick&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'Backstage passes to a TAFKAL80ETC concert'&lt;/span&gt;
    &lt;span class="n"&gt;backstage_tick&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normal_tick&lt;/span&gt;
  &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;brie_tick&lt;/span&gt;
  &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;

  &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sulfuras_tick&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;backstage_tick&lt;/span&gt;
  &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;              &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="vi"&gt;@quality&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@days_remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me defer my real-life example to Section 3c. I will show how to update the flags code from &lt;a href="https://github.com/ember-codemods/ember-component-template-colocation-migrator"&gt;ember-component-template-colocation-migrator&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  b. Nested Promises
&lt;/h3&gt;

&lt;p&gt;Previously, we saw that a nested if statement can be hard to reason and modify. By the same token, we want to avoid nested promises.&lt;/p&gt;

&lt;p&gt;Here's a server code that I had written in my nascent days:&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;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/my-example&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* Query options omitted */&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="nx"&gt;user&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;// Get the user's profile&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataValues&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Serialize the user's stories&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;story&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;story&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;story&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullName&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;photos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;story&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Photos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;caption&lt;/span&gt;
        &lt;span class="p"&gt;}))&lt;/span&gt;
      &lt;span class="p"&gt;}));&lt;/span&gt;

      &lt;span class="c1"&gt;// Serialize the user's readers&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;readers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Readers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;readerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reader_id&lt;/span&gt;
      &lt;span class="p"&gt;}));&lt;/span&gt;

      &lt;span class="c1"&gt;// Serialize the user's writers&lt;/span&gt;
      &lt;span class="nx"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* Query options omitted */&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="nx"&gt;results&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;const&lt;/span&gt; &lt;span class="nx"&gt;writers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;writerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writer_id&lt;/span&gt;
          &lt;span class="p"&gt;}));&lt;/span&gt;

          &lt;span class="c1"&gt;// Send the user's profile, stories, readers, and writers&lt;/span&gt;
          &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&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;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;stories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;readers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;writers&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The end goal is remarkably simple: Send to a client what we know about the user (lines 35-40). So why does the code feel like mess?&lt;/p&gt;

&lt;p&gt;One reason is nested promises. (There's another and we will address it in Section 3a.) With so many indentations, it's difficult to see where code begins and ends, and which variables cross over from one promise to another. In addition, the code assumes no failure points.&lt;/p&gt;

&lt;p&gt;Prior to the wide adoption of &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt;, we might have used a promise chain to refactor this code. &lt;a href="https://philipwalton.com/articles/untangling-deeply-nested-promise-chains/"&gt;A promise chain isn't without problems&lt;/a&gt;, however.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt;, we can rewrite the code as follows:&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;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/my-example&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* Query options omitted */&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Could not find user.&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;// Get user's profile&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataValues&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Serialize user's stories&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;story&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;story&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;story&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullName&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;photos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;story&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Photos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;caption&lt;/span&gt;
      &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

    &lt;span class="c1"&gt;// Serialize user's readers&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;readers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Readers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;readerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reader_id&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

    &lt;span class="c1"&gt;// Serialize user's writers&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* Query options omitted */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;writers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;writerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writer_id&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

    &lt;span class="c1"&gt;// Send the user's profile, stories, readers, and writers&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&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;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;stories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;readers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;writers&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* Error handling omitted */&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;Notice that &lt;code&gt;profile&lt;/code&gt;, &lt;code&gt;stories&lt;/code&gt;, &lt;code&gt;readers&lt;/code&gt;, and &lt;code&gt;writers&lt;/code&gt; are now defined at the same indentation level. This helps us trace the ending of the story that the code tells. In the end, we send data to the client, but where do they come from? Let's scroll up.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Extract Functions
&lt;/h2&gt;

&lt;p&gt;Now for the grand finale. At times, you may encounter a function that does 1 thing (this is good) but has many lines of code (likely bad). In fact, you saw one in Section 2b.&lt;/p&gt;

&lt;p&gt;The function contains a few key steps that run in sequence. Your goals are to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identify the key steps&lt;/li&gt;
&lt;li&gt;Create a function for each step&lt;/li&gt;
&lt;li&gt;Assign each function a descriptive name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This process of &lt;strong&gt;breaking a large function into smaller ones&lt;/strong&gt; is called extraction. Some of the smaller functions, especially if they don't require talking to an external system (e.g. make an API request or search an index), can now be unit-tested.&lt;/p&gt;

&lt;p&gt;If I were to pick the most useful refactoring technique, extraction would be it.&lt;/p&gt;

&lt;h3&gt;
  
  
  a. Example 1
&lt;/h3&gt;

&lt;p&gt;In Section 2b, we managed to remove nested promises. Let's refactor the code further by extracting functions.&lt;/p&gt;

&lt;p&gt;How do you identify the key steps? A good indicator is a comment that was left to describe what the code does. You may even name the function based on the comment.&lt;/p&gt;

&lt;p&gt;If I got to rewrite the API, I think it would look something like this:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;serialize&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="s1"&gt;../some-path&lt;/span&gt;&lt;span class="dl"&gt;'&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="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/my-example&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* query options omitted */&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Could not find user.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;serialize&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;Story&lt;/span&gt;&lt;span class="dl"&gt;'&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stories&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;readers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;serialize&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;Reader&lt;/span&gt;&lt;span class="dl"&gt;'&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Readers&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;writers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;getWriters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&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;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;stories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;readers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;writers&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* Handle error */&lt;/span&gt;

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

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getWriters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  b. Example 2
&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://github.com/ember-learn/whats-new-in-emberland"&gt;whats-new-in-emberland&lt;/a&gt;, I found the &lt;code&gt;model&lt;/code&gt; hook, a function that fetches PRs (pull requests) and RFCs (requests for comments), looking like this:&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="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startOfWeek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startOfWeek&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectFetches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CONSTANTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REPOS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;repo&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-organization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;orgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectFetches&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prFetches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;orgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;org&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.github.com/search/issues?q=is:pr+org:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;+created:&amp;gt;=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startOfWeek&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YYYY-MM-DD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&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;headers&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;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`token &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;githubSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;githubAccessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="nx"&gt;then&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="o"&gt;=&amp;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;json&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="nx"&gt;pulls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pushPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-pull&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="na"&gt;githubPull&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rfcFetches&lt;/span&gt; &lt;span class="o"&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;ember-cli/rfcs&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="s1"&gt;emberjs/rfcs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;repo&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-pull&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;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prFetches&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pulls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;peekAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-pull&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rfcSets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rfcFetches&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;mergedPulls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pulls&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="nx"&gt;pull&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mergedAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startOfWeek&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;previousValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;previousValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;newPulls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pulls&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="nx"&gt;pull&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startOfWeek&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mergedAt&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;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;previousValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;previousValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;newRfcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rfcSets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;pulls&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pulls&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="nx"&gt;pull&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startOfWeek&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;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;previousValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;previousValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;mergedRfcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rfcSets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;pulls&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pulls&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="nx"&gt;pull&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mergedAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startOfWeek&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;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;previousValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;previousValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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;hash&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;orgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;mergedPulls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;newPulls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;mergedRfcs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;newRfcs&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;The key to refactoring &lt;code&gt;model&lt;/code&gt; was to extract functions one at a time. From lines 5-19 and 25-26, I understood that &lt;code&gt;model&lt;/code&gt; fetches PRs. That's great! I &lt;a href="https://github.com/ember-learn/whats-new-in-emberland/pull/43/commits/446d4edd3444ccd151e38629eeab2715a073b61e"&gt;extracted a function&lt;/a&gt;. Similarly, from lines 21-23 and 27, I saw that &lt;code&gt;model&lt;/code&gt; fetches RFCs. That's yet another &lt;a href="https://github.com/ember-learn/whats-new-in-emberland/pull/43/commits/9efac8bd0199b8887930aad8542099bef7670ef1"&gt;extraction&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It was interesting that extracting functions from lines 29-47 (a total of 4 functions) did require knowledge of Ember in order to refactor effectively. In addition to the &lt;code&gt;model&lt;/code&gt; hook, Ember provides the &lt;code&gt;setupController&lt;/code&gt; hook. It allows us to post-process data from &lt;code&gt;model&lt;/code&gt;. For example, we can filter arrays.&lt;/p&gt;

&lt;p&gt;I moved lines 29-47 to &lt;code&gt;setupController&lt;/code&gt; for better separation of concerns, extracted functions, then further simplified code. In the end, I uncovered this beautiful code:&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;model&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;hash&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;prs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetchPRs&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;rfcs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetchRFCs&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;setupController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setupController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;prs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rfcs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mergedPRs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filterMerged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newPRs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filterNew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mergedRFCs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filterMerged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rfcs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newRFCs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filterNew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rfcs&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;h3&gt;
  
  
  c. Example 3
&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://github.com/ember-codemods/ember-component-template-colocation-migrator"&gt;ember-component-template-colocation-migrator&lt;/a&gt;, I &lt;a href="https://github.com/ember-codemods/ember-component-template-colocation-migrator/pull/11/files/590a2b1754fad5c5b69974bdb0f4dbc4ff8ab2b8..e264611827ea39fb0028412dbece6e156789dd16"&gt;extracted a few functions&lt;/a&gt; from the main function, &lt;code&gt;execute&lt;/code&gt;, before I added a feature. As a result, the feature caused a small, predictable change to &lt;code&gt;execute&lt;/code&gt; (lines 9-10 below):&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="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;templateFilePaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findClassicComponentTemplates&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;templateFilePaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;skipTemplatesUsedAsLayoutName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templateFilePaths&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;templateFilePaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;skipTemplatesUsedAsPartial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templateFilePaths&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;structure&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flat&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changeComponentStructureToFlat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templateFilePaths&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;structure&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nested&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changeComponentStructureToNested&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templateFilePaths&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;removeEmptyClassicComponentDirectories&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;Another example—one that hasn't been done (it's up for grabs for Hacktoberfest!)—is to extract a function from the flags code that we saw earlier:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;argv&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yargs&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="nx"&gt;getStructure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;changeToFlatStructure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;changeToNestedStructure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&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;changeToFlatStructure&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flat&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;changeToNestedStructure&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nested&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="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flat&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;Notice the early exits, the refactoring technique that we learned in Section 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Conclusion
&lt;/h2&gt;

&lt;p&gt;You can make an impact on an open source project by refactoring code. By practicing just 3 techniques—&lt;strong&gt;rename things, remove nests, and extract functions&lt;/strong&gt;—you can help new contributors understand code and increase the longevity of the project.&lt;/p&gt;

&lt;p&gt;You witnessed a few examples of what code can be like when you take good care of it. I encourage you to apply what you learned and share these techniques with others.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>tutorial</category>
      <category>beginners</category>
      <category>ember</category>
    </item>
    <item>
      <title>Container Queries: Cross-Resolution Testing</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Mon, 08 Jun 2020 13:07:49 +0000</pubDate>
      <link>https://dev.to/ijlee2/container-queries-cross-resolution-testing-jh8</link>
      <guid>https://dev.to/ijlee2/container-queries-cross-resolution-testing-jh8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://crunchingnumbers.live" rel="noopener noreferrer"&gt;crunchingnumbers.live&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Failure&lt;/em&gt; to test is why I started to work on &lt;a href="https://github.com/ijlee2/ember-container-query/" rel="noopener noreferrer"&gt;ember-container-query&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A few months ago, my team and I introduced &lt;a href="https://github.com/chadian/ember-fill-up" rel="noopener noreferrer"&gt;ember-fill-up&lt;/a&gt; to our apps. It worked well but we noticed something strange. Percy snapshots that were taken at a mobile width would show &lt;code&gt;ember-fill-up&lt;/code&gt; using the desktop breakpoint. They didn't match what we were seeing on our browsers.&lt;/p&gt;

&lt;p&gt;For a while, we ignored this issue because our CSS wasn't great. We performed some tricks with &lt;code&gt;flex&lt;/code&gt; and &lt;code&gt;position&lt;/code&gt; that could have affected Percy snapshots. Guess what happened when we switched to &lt;code&gt;grid&lt;/code&gt; and improved the document flow. We still saw incorrect Percy snapshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Whodunit? ember-qunit.
&lt;/h2&gt;

&lt;p&gt;To eliminate &lt;code&gt;ember-fill-up&lt;/code&gt; as a suspect, I used modifiers to recreate the addon. To my surprise and distress, using modifiers didn't fix the problem. After many trial-and-errors, I found the culprit: &lt;code&gt;ember-qunit&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2020%2F06%2Fmeme-1.jpg" 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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2020%2F06%2Fmeme-1.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
TIL Morpheus never said "What if I told you." Were the past 20 years real?



&lt;p&gt;By default, &lt;code&gt;ember-qunit&lt;/code&gt; &lt;a href="https://github.com/emberjs/ember-qunit/blob/master/vendor/ember-qunit/test-container-styles.css#L32-L37" rel="noopener noreferrer"&gt;scales the test window&lt;/a&gt; so that your app fits inside its test container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nf"&gt;#ember-testing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;transform-origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;top&lt;/span&gt; &lt;span class="nb"&gt;left&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;What does this mean? When you write tests, you cannot trust DOM render decisions that are based on width or height. Decisions made by media queries and addons like &lt;code&gt;ember-container-query&lt;/code&gt;, &lt;code&gt;ember-fill-up&lt;/code&gt;, &lt;code&gt;ember-responsive&lt;/code&gt;, and &lt;code&gt;ember-screen&lt;/code&gt;. Because what your test saw differed from what you saw on a browser, you might have had to mock a service (fake the window size) to get certain elements to (dis)appear.&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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2020%2F06%2Ftest-comparisons.jpg" 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%2Fcrunchingnumbersdotlive.files.wordpress.com%2F2020%2F06%2Ftest-comparisons.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
Percy snapshots on top row are what we get with default &lt;code&gt;ember-qunit&lt;/code&gt;. The snapshots on the middle row are what we want. There seem to be larger differences in smaller windows. (&lt;a href="https://crunchingnumbersdotlive.files.wordpress.com/2020/06/test-comparisons.jpg" rel="noreferrer noopener"&gt;Full screen image&lt;/a&gt;)



&lt;p&gt;Fortunately, there is an &lt;a href="https://github.com/emberjs/ember-qunit/blob/master/vendor/ember-qunit/test-container-styles.css#L39-L44" rel="noopener noreferrer"&gt;escape hatch&lt;/a&gt;. We can apply the &lt;code&gt;.full-screen&lt;/code&gt; class to the test container (&lt;code&gt;#ember-testing-container&lt;/code&gt;) to undo the scaling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.full-screen&lt;/span&gt; &lt;span class="nf"&gt;#ember-testing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&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;As an aside, this class is applied when we enable &lt;a href="https://github.com/ember-cli/ember-cli-qunit/pull/120" rel="noopener noreferrer"&gt;development mode&lt;/a&gt;, a relatively unknown feature.&lt;/p&gt;

&lt;p&gt;In my opinion and conjecture, we (Ember community) didn't really notice this problem and fix it because we were used to writing tests only at 1 resolution: the 1440 × 900 px desktop. We are also prone to designing the web for desktop first. Had we been able to test multiple resolutions easily, I think today's state of testing would be even better.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Cross-Resolution Testing
&lt;/h2&gt;

&lt;p&gt;How can you test your app and addon at multiple resolutions?&lt;/p&gt;

&lt;p&gt;We need to be able to mark tests that can only be run at one resolution. After all, there will be user workflows that make sense only on mobile or tablet, for example. My team and I followed the Octane craze and introduced &lt;strong&gt;filters&lt;/strong&gt; that look like decorators:&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;// Mobile only&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mobile A user can do X in Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Tablet only&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tablet A user can do X in Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Any resolution&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A user can do X in Dashboard&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;Let's go over how to update your test setup, configure CI, and write a Percy test helper to allow these filters.&lt;/p&gt;

&lt;p&gt;I'll use &lt;a href="https://crunchingnumbers.live/2020/03/17/ci-with-github-actions-for-ember-apps/" rel="noopener noreferrer"&gt;GitHub Actions for CI&lt;/a&gt;. Describing each line of code can get boring so I'll convey the idea and simplify code in many cases. I encourage you to look at &lt;a href="https://crunchingnumbers.live/2020/03/17/ci-with-github-actions-for-ember-apps/" rel="noopener noreferrer"&gt;ember-container-query&lt;/a&gt; to study details and use my latest code.&lt;/p&gt;

&lt;h3&gt;
  
  
  a. testem.js
&lt;/h3&gt;

&lt;p&gt;We'll start by updating &lt;code&gt;testem.js&lt;/code&gt;. It is responsible for setting the window size.&lt;/p&gt;

&lt;p&gt;The idea is to dynamically set the window size based on an environment variable. I'll call this variable &lt;code&gt;DEVICE&lt;/code&gt;.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FILTERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;mobile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/^(?=(.*Acceptance))(?!(.*@tablet|.*@desktop))/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tablet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/^(?=(.*Acceptance))(?!(.*@mobile|.*@desktop))/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/^(?!(.*@mobile|.*@tablet))/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WINDOW_SIZES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;mobile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;400,900&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tablet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;900,900&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1400,900&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DEVICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desktop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FILTERS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DEVICE&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;windowSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WINDOW_SIZES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DEVICE&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;windowSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;test_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`tests/index.html?filter=&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="s2"&gt;&amp;amp;amp;width=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;amp;height=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;browser_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;`--window-size=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;windowSize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From lines 15-16, we see that &lt;code&gt;DEVICE&lt;/code&gt; decides how tests are run. In QUnit, we can use regular expressions to filter tests. I used &lt;strong&gt;lookaheads&lt;/strong&gt; to say, "When &lt;code&gt;DEVICE=mobile&lt;/code&gt;, only run application tests with &lt;code&gt;@mobile&lt;/code&gt; filter or application tests without any filter." I decided to run rendering and unit tests only when &lt;code&gt;DEVICE=desktop&lt;/code&gt; because they are likely independent of window size.&lt;/p&gt;

&lt;p&gt;On line 20, the query parameters &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; are extra and have an important role. I'll explain why they are needed when we write the test helper for Percy.&lt;/p&gt;

&lt;h3&gt;
  
  
  b. Reset Viewport
&lt;/h3&gt;

&lt;p&gt;Next, we need to apply the &lt;code&gt;.full-screen&lt;/code&gt; class to the test container.&lt;/p&gt;

&lt;p&gt;There are two options. We can create a test helper if there are few application test files (likely for an addon), or an initializer if we have many (likely for an app).&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;// Test helper&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resetViewport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beforeEach&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;testingContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-testing-container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;testingContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full-screen&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="c1"&gt;// Initializer&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;config&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;my-app-name/config/environment&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initialize&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;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;testingContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-testing-container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;testingContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full-screen&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="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;initialize&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  c. package.json
&lt;/h3&gt;

&lt;p&gt;The last step for an MVP (minimum viable product) is to update the test scripts.&lt;/p&gt;

&lt;p&gt;Since Ember 3.17, &lt;code&gt;npm-run-all&lt;/code&gt; has been available to run scripts in parallel. I'll assume that you also have &lt;code&gt;ember-exam&lt;/code&gt; and &lt;code&gt;@percy/ember&lt;/code&gt;.&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;"scripts"&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;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm-run-all --parallel test:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:desktop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"percy exec -- ember exam --test-port=7357"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:mobile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEVICE=mobile percy exec -- ember exam --test-port=7358"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:tablet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEVICE=tablet percy exec -- ember exam --test-port=7359"&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;p&gt;In addition to setting &lt;code&gt;DEVICE&lt;/code&gt;, it's crucial to use different port numbers. Now, we can run &lt;code&gt;yarn test&lt;/code&gt; to check our app at 3 window sizes. If you have disparate amounts of tests for desktop, mobile, and tablet, you can set different &lt;code&gt;--split&lt;/code&gt; values so that you allocate more partitions to one window size. For example, 4 partitions to desktop, 2 to mobile, and 1 to tablet.&lt;/p&gt;

&lt;h3&gt;
  
  
  d. CI
&lt;/h3&gt;

&lt;p&gt;Your code change may depend on what features your CI provider offers and how many &lt;code&gt;ember-exam&lt;/code&gt; partitions you used to test a window size. I don't know what your CI looks like right now, so I'll make a handwave.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;ember-container-query&lt;/code&gt;, I didn't split tests into multiple partitions. There simply weren't that many. As a result, I was able to use &lt;code&gt;matrix&lt;/code&gt; to simplify the workflow:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test-addon&lt;/span&gt;&lt;span class="pi"&gt;:&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;device&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;desktop&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;mobile&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;tablet&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test&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;percy/exec-action@v0.3.0&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;custom-command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test:${{ matrix.device }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  e. Test Helper for Percy
&lt;/h3&gt;

&lt;p&gt;The end is the beginning is the end. We want to write a test helper for Percy, the thing that launched me into a journey of discovery.&lt;/p&gt;

&lt;p&gt;In its simplest form, the test helper understands filters and knows the window size. It also generates a unique snapshot name that is human readable.&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;percySnapshot&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;@percy/ember&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;takeSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qunitAssert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qunitAssert&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getWindowSize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;percySnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;widths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qunitAssert&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getWindowSize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;width&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On line 13, I hid implementation details. The idea is to transform QUnit's &lt;code&gt;assert&lt;/code&gt; object into a string.&lt;/p&gt;

&lt;p&gt;Line 16 is the interesting bit. Earlier, when we updated &lt;code&gt;testem.js&lt;/code&gt;, I mentioned passing width and height as query parameters. I tried two other approaches before.&lt;/p&gt;

&lt;p&gt;In my first attempt, I stored &lt;code&gt;process.env.DEVICE&lt;/code&gt; in &lt;code&gt;config/environment.js&lt;/code&gt; and imported the file to the test helper file. From &lt;code&gt;WINDOW_SIZES&lt;/code&gt;, one can find out width and height from &lt;code&gt;DEVICE&lt;/code&gt;. For QUnit, this worked. For Percy, it did not. Since &lt;code&gt;v2.x&lt;/code&gt;, Percy doesn't hook into the Ember build pipeline so &lt;code&gt;DEVICE&lt;/code&gt; was &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In my second attempt, I used &lt;code&gt;window.innerWidth&lt;/code&gt; and &lt;code&gt;window.innerHeight&lt;/code&gt; to get direct measurements. &lt;code&gt;innerWidth&lt;/code&gt; gave the correct width, but &lt;code&gt;innerHeight&lt;/code&gt; turned out to be unreliable. Because I wanted to test at multiple widths and multiple heights, I rejected this approach as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. How to Run Tests
&lt;/h2&gt;

&lt;p&gt;After we make these changes, an important question remains. How do we run tests locally?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;yarn test&lt;/code&gt; to run all desktop, mobile, and tablet tests in parallel&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;yarn test:desktop --server&lt;/code&gt; to run all desktop tests with &lt;code&gt;--server&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEVICE=mobile ember test --filter='@mobile A user can do X in Dashboard'&lt;/code&gt; to run a particular test&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. What's Next?
&lt;/h2&gt;

&lt;p&gt;On the long horizon, I'd like us to reexamine and change why we are currently limited to testing 1 resolution. Ember's testing story is an already amazing one. I believe the ability to test multiple resolutions (and do so easily without taking 5 steps like above) would make that story even better.&lt;/p&gt;

&lt;p&gt;For nearer goals, I'd like us to iron out a couple of issues in overriding &lt;code&gt;ember-qunit&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Even with &lt;code&gt;.full-screen&lt;/code&gt;, the test container's height can be off if we use &lt;code&gt;--server&lt;/code&gt; to launch the test browser. If assertions sometimes fail due to incorrect window size, it's harder to separate true and false positives.&lt;/li&gt;
&lt;li&gt;Visiting &lt;code&gt;localhost:4200/tests&lt;/code&gt; to start tests will also throw off the test container's width and height. It may be impractical to ask developers to run tests with &lt;code&gt;--server&lt;/code&gt; because this does not launch Ember Inspector.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We need to look at allowing cross-resolution testing for &lt;code&gt;ember-mocha&lt;/code&gt; as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Notes
&lt;/h2&gt;

&lt;p&gt;Special thanks to my team Sean Massa, Trek Glowacki, and Saf Suleman for trying out a dangerously unproven, new testing approach with me.&lt;/p&gt;

</description>
      <category>ember</category>
      <category>javascript</category>
      <category>testing</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Container Queries: Adaptive Images</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Thu, 04 Jun 2020 12:20:36 +0000</pubDate>
      <link>https://dev.to/ijlee2/container-queries-adaptive-images-264l</link>
      <guid>https://dev.to/ijlee2/container-queries-adaptive-images-264l</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="http://crunchingnumbers.live/"&gt;crunchingnumbers.live&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A powerful application of container queries is &lt;strong&gt;adaptive images&lt;/strong&gt;. It's about striking the balance between displaying an image that looks good (the larger the better) and one that loads fast (the smaller the better).&lt;/p&gt;

&lt;p&gt;Currently, we are limited to &lt;a href="https://developer.mozilla.org/docs/Web/API/HTMLImageElement/srcset"&gt;&lt;code&gt;srcset&lt;/code&gt;&lt;/a&gt;, which selects the optimal image based on the global screen size. This may work for splash images that cover the whole width, but what about images that we show in a partial area?&lt;/p&gt;

&lt;p&gt;Enter &lt;a href="https://github.com/ijlee2/ember-container-query"&gt;container query&lt;/a&gt;. We can compare every candidate image's width, height, and aspect ratio (I assume that we have these information via metadata) to those of the container, then use the best fitting image's URL.&lt;/p&gt;

&lt;p&gt;In practice, we &lt;strong&gt;create a component&lt;/strong&gt; and pass an array of images. (Here, by an image, I mean a POJO with just URL and metadata, not the actual image.)&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;ContainerQuery&lt;/span&gt; &lt;span class="na"&gt;as&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;CQ&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
    &lt;span class="na"&gt;local-class=&lt;/span&gt;&lt;span class="s"&gt;"image-container"&lt;/span&gt;
    &lt;span class="err"&gt;{{&lt;/span&gt;&lt;span class="na"&gt;did-update&lt;/span&gt;
      &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;fn&lt;/span&gt; &lt;span class="na"&gt;this.setImageSource&lt;/span&gt; &lt;span class="na"&gt;CQ.dimensions&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
      &lt;span class="na"&gt;CQ.dimensions&lt;/span&gt;
    &lt;span class="err"&gt;}}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;local-class=&lt;/span&gt;&lt;span class="s"&gt;"image"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;{{this.src}}&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;/ContainerQuery&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the backing class, &lt;code&gt;setImageSource&lt;/code&gt; calls &lt;code&gt;findBestFittingImage&lt;/code&gt; to set &lt;code&gt;this.src&lt;/code&gt;. The latter function exists in a utility so that we can write fast unit tests.&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;findBestFittingImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;containerDimensions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;containerDimensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imagesRanked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&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;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageAspectRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imageWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;arMetric&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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageAspectRatio&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hwMetric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;imageHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hwTiebreaker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;imageHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;arMetric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hwMetric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hwMetric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;Infinity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hwMetric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;hwTiebreaker&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;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arMetric&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arMetric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arMetric&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arMetric&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;-&lt;/span&gt;&lt;span class="mi"&gt;1&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hwMetric&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hwMetric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hwMetric&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hwMetric&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;-&lt;/span&gt;&lt;span class="mi"&gt;1&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hwTiebreaker&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hwTiebreaker&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;imagesRanked&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;url&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;The formulas for &lt;code&gt;arMetric&lt;/code&gt;, &lt;code&gt;hwMetric&lt;/code&gt;, and &lt;code&gt;hwTiebreaker&lt;/code&gt; aren't anything special. I'm using &lt;em&gt;l^p&lt;/em&gt; norms to quantify the difference between an image and the container. I can put them into words by saying I'm making 3 assumptions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Users prefer images whose aspect ratio is close to the container's.&lt;/li&gt;
&lt;li&gt;Users prefer images whose height and width are larger than the container's.&lt;/li&gt;
&lt;li&gt;If all images are smaller than the container, users want the image that comes closest to the container.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it! With a bit of JavaScript and math, we solved a problem that MDN says is not possible (I paraphrased):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[H]ence the need to implement solutions like &lt;code&gt;srcset&lt;/code&gt;. For example, you couldn't load the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; element, then detect the viewport width with JavaScript, and then dynamically change the source image to a smaller one if desired. By then, the original image would already have been loaded, and you would load the small image as well, which is even worse in responsive image terms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images"&gt;Why can't we just do this using CSS or JavaScript? - MDN&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here are my &lt;a href="https://github.com/ijlee2/ember-container-query/tree/master/tests/dummy/app/components/widgets/widget-3/tour-schedule/responsive-image"&gt;code for adaptive images&lt;/a&gt; and &lt;a href="https://github.com/ijlee2/ember-container-query/blob/master/tests/unit/utils/widgets/widget-3-test.js"&gt;tests&lt;/a&gt;. There's more to do so I encourage you to extend my work. I made an ideal assumption about where I'm getting image URLs and metadata. Do things change when they come from a network request? Are there better ranking algorithms? I look forward to see what you can do with &lt;a href="https://github.com/ijlee2/ember-container-query"&gt;container queries&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ember</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>css</category>
    </item>
    <item>
      <title>Container Queries: Reimagined</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Tue, 02 Jun 2020 03:27:27 +0000</pubDate>
      <link>https://dev.to/ijlee2/container-queries-reimagined-2a6d</link>
      <guid>https://dev.to/ijlee2/container-queries-reimagined-2a6d</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://crunchingnumbers.live/2020/06/01/container-queries-reimagined/" rel="noopener noreferrer"&gt;crunchingnumbers.live&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On Friday, I published my first addon. It's called &lt;a href="https://github.com/ijlee2/ember-container-query/" rel="noopener noreferrer"&gt;ember-container-query&lt;/a&gt;. Becoming an addon author was one of my goals for 2020, so I'm especially proud of it and hope that you will find a good use.&lt;/p&gt;

&lt;p&gt;Container queries aren't new in Ember. My addon is based on Chad Carbert's &lt;a href="https://github.com/chadian/ember-fill-up" rel="noopener noreferrer"&gt;ember-fill-up&lt;/a&gt; from 2019. That, in turn, credits Andrey Mikhaylov's &lt;a href="https://github.com/lolmaus/ember-element-query" rel="noopener noreferrer"&gt;ember-element-query&lt;/a&gt; from 2017. I even found a &lt;a href="https://gregbabiars.com/responsive-ember-components/" rel="noopener noreferrer"&gt;blog post&lt;/a&gt; from 2015, by Greg Babiars!&lt;/p&gt;

&lt;p&gt;Simplicity is what makes &lt;code&gt;ember-container-query&lt;/code&gt; different from the previous tries. I combined 2 atomic solutions (&lt;strong&gt;modifiers&lt;/strong&gt;, introduced in Ember Octane) to arrive at &lt;em&gt;the&lt;/em&gt; atomic solution to container queries.&lt;/p&gt;

&lt;p&gt;Because I practiced code composition and provided the minimum necessary API, the benefits are twofold. I have fewer code to maintain, while you have a choice to build your code on top of my addon or another with a similar API.&lt;/p&gt;

&lt;p&gt;Testing is the second differentiator. With every code change, the &lt;a href="https://crunchingnumbers.live/2020/03/17/ci-with-github-actions-for-ember-apps/" rel="noopener noreferrer"&gt;CI&lt;/a&gt; checks that my addon and demo app work &lt;strong&gt;no matter the window size&lt;/strong&gt;. Testing multiple windows is &lt;em&gt;kinda&lt;/em&gt; important for container queries.&lt;/p&gt;

&lt;p&gt;To my knowledge, no Ember app or addon has tried testing multiple windows and publicly released their solution. In the next article, I will go over how you can update the default test setup to achieve this feat.&lt;/p&gt;

&lt;p&gt;In the meantime, I encourage you to learn &lt;a href="https://github.com/ijlee2/ember-container-query#applications" rel="noopener noreferrer"&gt;what container queries can do for you&lt;/a&gt; and get inspiration from my &lt;a href="https://ember-container-query.netlify.app/" rel="noopener noreferrer"&gt;demo app&lt;/a&gt;. The code is publicly available on &lt;a href="https://github.com/ijlee2/ember-container-query/tree/master/tests/dummy/app" rel="noopener noreferrer"&gt;GitHub&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%2Fuser-images.githubusercontent.com%2F16869656%2F82177207-72699c00-989e-11ea-9cb6-2e388c5e98c0.gif" 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%2Fuser-images.githubusercontent.com%2F16869656%2F82177207-72699c00-989e-11ea-9cb6-2e388c5e98c0.gif" alt="Demo of ember-container-query"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ember</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>CI with GitHub Actions for Ember Apps</title>
      <dc:creator>Isaac Lee</dc:creator>
      <pubDate>Tue, 17 Mar 2020 17:35:08 +0000</pubDate>
      <link>https://dev.to/ijlee2/ci-with-github-actions-for-ember-apps-oim</link>
      <guid>https://dev.to/ijlee2/ci-with-github-actions-for-ember-apps-oim</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://crunchingnumbers.live/2020/03/17/ci-with-github-actions-for-ember-apps/"&gt;crunchingnumbers.live&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lately I've been working on &lt;a href="https://ember-music.herokuapp.com/"&gt;Ember Music&lt;/a&gt;, an app that I can use as a playground to test addons and ideas in Ember. When I need to write a blog post, I can reach for this app instead of designing a new one each time. Since the app will grow over time, I wanted to introduce &lt;strong&gt;continuous integration&lt;/strong&gt; (CI) and &lt;strong&gt;continuous deployment&lt;/strong&gt; early.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.heroku.com/"&gt;Heroku Dashboard&lt;/a&gt; makes deploying code on GitHub simple. From the Deploy tab, you select GitHub, find your repo, then check "Wait for CI to pass before deploy."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pQbXxSUr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/introduction_heroku.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pQbXxSUr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/introduction_heroku.png" alt="" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
Heroku makes deploying code on GitHub simple.



&lt;p&gt;For continuous integration, I tried out &lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt; since it is free (there are limits to minutes and storage for private repos) and my code is on GitHub. I also wanted to find an alternative to Codeship Pro that I use for work. One app has about 150 tests, but CI time wildly varies between 3 and 15 minutes. Because ten minutes is how long CI took for a larger app that I had worked on, I haven't been content.&lt;/p&gt;

&lt;p&gt;With GitHub Actions, I was able to make a &lt;strong&gt;workflow&lt;/strong&gt; that did everything I want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set operating system and Node version&lt;/li&gt;
&lt;li&gt;Cache ​dependencies (avoid &lt;code&gt;yarn install&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Lint files and dependencies&lt;/li&gt;
&lt;li&gt;Run tests separately from linting&lt;/li&gt;
&lt;li&gt;Split tests and run in parallel&lt;/li&gt;
&lt;li&gt;Take &lt;a href="https://docs.percy.io/docs/ember"&gt;Percy&lt;/a&gt; snapshots in parallel&lt;/li&gt;
&lt;li&gt;Be cost effective&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this blog post, I will share my workflow because there is a high chance that you, too, want to solve the problems listed above. Rather than dumping the entire workflow on you, I will start with a simple one and let it organically grow. Throughout, I will assume that you use &lt;code&gt;yarn&lt;/code&gt; to manage packages. If you use &lt;code&gt;npm&lt;/code&gt;, please check the GitHub Gist at the end to see the differences.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. I Want to Run Tests
&lt;/h1&gt;

&lt;p&gt;Testing is available to every Ember app and is integral to CI, so let's look at how to write a workflow that runs &lt;code&gt;ember test&lt;/code&gt;. Along the way, you will see how to set the operating system and Node version.&lt;/p&gt;

&lt;h2&gt;
  
  
  a. Create Workflow
&lt;/h2&gt;

&lt;p&gt;In the root of your project, create folders called &lt;code&gt;.github&lt;/code&gt; and &lt;code&gt;.github/workflows&lt;/code&gt;. All workflows must be stored in &lt;code&gt;.github/workflows&lt;/code&gt;. Workflows are written in YAML, so let's create a file called &lt;code&gt;ci.yml&lt;/code&gt;.&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;# Folder structure&lt;/span&gt;

ember-music
│
├── .github
│   │
│   └── workflows
│       │
│       └── ci.yml
│
├── app
│
│   ...
│
├── tests
│
│   ...
│
├── package.json
│
└── yarn.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the file, we can use the &lt;code&gt;on&lt;/code&gt; and &lt;code&gt;jobs&lt;/code&gt; keys to specify when CI runs and what it does. We can also give the workflow a &lt;code&gt;name&lt;/code&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;# File: .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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&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 commit and push this file, the workflow will fail in an instant. (GitHub does notify you by email.) On GitHub, let's click on the Actions tab, then find the workflow to see what went wrong. The error message shows that we haven't defined jobs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RFgA7lsH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_1a_github_actions.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RFgA7lsH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_1a_github_actions.png" alt="" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;
GitHub Actions alerts us we haven't defined jobs.



&lt;h2&gt;
  
  
  b. Define Jobs
&lt;/h2&gt;

&lt;p&gt;A workflow must have one or more jobs to do. A &lt;strong&gt;job&lt;/strong&gt; is completed by following a set of &lt;code&gt;steps&lt;/code&gt;. At each &lt;strong&gt;step&lt;/strong&gt;, we can &lt;code&gt;run&lt;/code&gt; a command or &lt;code&gt;use&lt;/code&gt; an action (custom or imported) to do something meaningful—something that gets us closer to finishing the job.&lt;/p&gt;

&lt;p&gt;When someone makes a push or pull request, a CI's job is to run tests. Think about what steps you take to test someone else's Ember app. Likely, you would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repo.&lt;/li&gt;
&lt;li&gt;Set the Node version, maybe with &lt;code&gt;nvm&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;yarn&lt;/code&gt; to install dependencies.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;ember test&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Guess what? We can tell a workflow to do the same!&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;# File: .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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&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;test&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&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;${{ matrix.os }}&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;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;12.x&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out a copy of the repo&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use Node.js ${{ matrix.node-version }}&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-node@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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.node-version }}&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 dependencies&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install --frozen-lockfile&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;Test Ember app&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because checking out a repo and setting up Node are common tasks, GitHub Actions provides actions that you can just call. The &lt;code&gt;matrix&lt;/code&gt; key lets you run the workflow on various operating systems and Node versions. Since I'm writing the app for myself, I specified one OS and Node version. If you are developing an addon for other people, you will likely specify more (take &lt;a href="https://github.com/ember-cli/ember-try"&gt;Ember versions&lt;/a&gt; into account, too).&lt;/p&gt;

&lt;p&gt;You may have noticed that I ran &lt;code&gt;yarn test&lt;/code&gt;. I did so because &lt;code&gt;package.json&lt;/code&gt; provides a script called &lt;code&gt;test&lt;/code&gt;. In Ember 3.16, these are the default scripts:&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;// File: package.json&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;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;ember build --environment=production&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;lint:hbs&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;ember-template-lint .&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;lint:js&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;eslint .&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;start&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;ember serve&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;test&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;ember test&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In short, running &lt;code&gt;yarn test&lt;/code&gt; means running &lt;code&gt;ember test&lt;/code&gt;. By relying on the scripts in &lt;code&gt;package.json&lt;/code&gt;, CI can check our code in the same manner as we might locally. We'll update these scripts as we expand the workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ra7anKrV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_1b_github_actions.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ra7anKrV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_1b_github_actions.png" alt="" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
GitHub Actions can run Ember tests.



&lt;h2&gt;
  
  
  c. When Should CI Run?
&lt;/h2&gt;

&lt;p&gt;In the sections above and below, I used &lt;code&gt;on: [push, pull_request]&lt;/code&gt; for simplicity.&lt;/p&gt;

&lt;p&gt;For a production app where you would create branches, make pull requests (PRs), then merge to &lt;code&gt;master&lt;/code&gt; branch, consider instead:&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;# File: .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;on&lt;/span&gt;&lt;span class="pi"&gt;:&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;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;
    &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, your CI will run according to these rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you create a branch and make a push, CI will not run.&lt;/li&gt;
&lt;li&gt;If you create a PR for that branch (draft or open), CI will run. GitHub Actions shows the run type to be &lt;code&gt;pull_request&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Marking a draft PR as ready (open) will not trigger CI again. 👍&lt;/li&gt;
&lt;li&gt;Any additional pushes that you make to the PR will trigger CI. (type: &lt;code&gt;pull_request&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;If you merge the PR into &lt;code&gt;master&lt;/code&gt;, CI will run once more. (type: &lt;code&gt;push&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  2. I Want to Lint
&lt;/h1&gt;

&lt;p&gt;A CI can also lint files and dependencies. Before the app becomes large and unwieldy, we want to ensure that our code follows a standard and relies on a single version for each package.&lt;/p&gt;

&lt;p&gt;Rather than adding a step to our existing job, we can create 2 jobs—one for linting and another for running tests—so that they can run in parallel. In GitHub Actions, we specify an extra job like this:&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;# File: .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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&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;lint&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;Lint files and dependencies&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;${{ matrix.os }}&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;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;12.x&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out a copy of the repo&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use Node.js ${{ matrix.node-version }}&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-node@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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.node-version }}&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 dependencies&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install --frozen-lockfile&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;lint:dependency&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn lint:dependency&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;lint:hbs&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn lint:hbs&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;lint:js&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn lint:js&lt;/span&gt;

    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although duplicate code (lines 14-23) are an eyesore, we'll repeat steps for simplicity—take baby steps to understanding GitHub Actions. At this point, we are more concerned by if the workflow will still pass than if GitHub Actions allows a "beforeEach hook." (The feature that'd let us DRY steps is called &lt;strong&gt;YAML anchor&lt;/strong&gt;. At the time of writing, &lt;a href="https://github.community/t5/GitHub-Actions/Support-for-YAML-anchors/m-p/30336"&gt;anchors are not supported&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;From line 26, you might guess that &lt;code&gt;package.json&lt;/code&gt; has an additional script. Indeed, it runs the addon &lt;a href="https://github.com/salsify/ember-cli-dependency-lint"&gt;ember-cli-dependency-lint&lt;/a&gt;.&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;// File: package.json&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;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;ember build --environment=production&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;lint:dependency&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;ember dependency-lint&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;lint:hbs&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;ember-template-lint .&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;lint:js&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;eslint .&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;start&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;ember serve&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;test&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;ember test --query=nolint&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, Ember QUnit lints if you have &lt;code&gt;ember-cli-eslint&lt;/code&gt;, &lt;code&gt;ember-cli-template-lint&lt;/code&gt;, or &lt;code&gt;ember-cli-dependency-lint&lt;/code&gt;. Now that we have a job dedicated to linting, I passed &lt;code&gt;--query=nolint&lt;/code&gt; so that the job for testing does not lint again.&lt;/p&gt;

&lt;p&gt;As an aside, starting with &lt;a href="https://blog.emberjs.com/2020/03/16/ember-3-17-released.html"&gt;Ember 3.17&lt;/a&gt;, you are advised to remove &lt;code&gt;ember-cli-eslint&lt;/code&gt; and &lt;code&gt;ember-cli-template-lint&lt;/code&gt; in favor of using &lt;code&gt;eslint&lt;/code&gt; and &lt;code&gt;ember-template-lint&lt;/code&gt;. The one exception is if you need &lt;em&gt;live&lt;/em&gt; linting. But chances are, you don't thanks to CI. You can now enjoy faster build and rebuild!&lt;/p&gt;

&lt;p&gt;Let's commit changes and push. When you see 2 green checks, let out that sigh.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--j5NhTenG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_2_github_actions-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--j5NhTenG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_2_github_actions-1.png" alt="" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
GitHub Actions can lint and run tests at the same time.



&lt;h1&gt;
  
  
  3. I Want to Run Tests in Parallel
&lt;/h1&gt;

&lt;p&gt;We can &lt;a href="https://crunchingnumbers.live/2019/10/11/write-tests-like-a-mathematician-part-3/"&gt;promote writing more tests&lt;/a&gt; if the time to run them can remain small. One way to achieve this is to split tests and run them in parallel using &lt;a href="https://ember-cli.com/ember-exam/"&gt;Ember Exam&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  a. Setup
&lt;/h2&gt;

&lt;p&gt;After you install &lt;code&gt;ember-exam&lt;/code&gt;, please open the file &lt;code&gt;tests/test-helper.js&lt;/code&gt;. You must replace the &lt;code&gt;start&lt;/code&gt; method from Ember QUnit (or Mocha) with the one from Ember Exam. Otherwise, running the command &lt;code&gt;ember exam&lt;/code&gt; has no effect.&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;// File: tests/test-helper.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Application&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;../app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;config&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;../config/environment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;setApplication&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="s1"&gt;@ember/test-helpers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;start&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;ember-exam/test-support/start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;setApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;setupTestIsolationValidation&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  b. Divide and Conquer
&lt;/h2&gt;

&lt;p&gt;By trial and error, I came up with a script that I hope works for you too:&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;// File: package.json&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;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;ember build --environment=production&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;lint:dependency&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;ember dependency-lint&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;lint:hbs&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;ember-template-lint .&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;lint:js&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;eslint .&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;start&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;ember serve&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;test&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;ember exam --query=nolint --split=4 --parallel=1&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote the script so that we can append flags to do useful things. With &lt;code&gt;yarn test --server&lt;/code&gt;, for example, you should see 4 browsers running. Good to have a sanity check. Each browser—a &lt;strong&gt;partition&lt;/strong&gt;—handles roughly a quarter of the tests. If you use QUnit, you can run &lt;code&gt;yarn test --server --random&lt;/code&gt; to check if your tests are order-dependent.&lt;/p&gt;

&lt;p&gt;Most importantly, the script allows us to append the &lt;code&gt;--partition&lt;/code&gt; flag so that GitHub Actions knows how to run Ember tests in parallel. Let's rename the job called &lt;code&gt;test&lt;/code&gt; to &lt;code&gt;test-partition-1&lt;/code&gt; and update its last step to run partition 1. Then, create three more jobs to run partitions 2 to 4.&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;# File: .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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&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;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-1&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 - Partition &lt;/span&gt;&lt;span class="m"&gt;1&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;${{ matrix.os }}&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;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;12.x&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out a copy of the repo&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use Node.js ${{ matrix.node-version }}&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-node@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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.node-version }}&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 dependencies&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install --frozen-lockfile&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;Test Ember app&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test --partition=1&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-4&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 - Partition &lt;/span&gt;&lt;span class="m"&gt;4&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;${{ matrix.os }}&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;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;12.x&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out a copy of the repo&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use Node.js ${{ matrix.node-version }}&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-node@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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.node-version }}&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 dependencies&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install --frozen-lockfile&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;Test Ember app&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test --partition=4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the workflow has 5 jobs. You can check that tests do run separately from linting and in parallel. You can also check that each partition has a different set of tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--g3onO5J---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_3b_github_actions.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--g3onO5J---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_3b_github_actions.png" alt="" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;
GitHub Actions can run tests in parallel.



&lt;p&gt;Unfortunately, everything isn't awesome. Each job has to run &lt;code&gt;yarn install&lt;/code&gt;, and this will happen every time we make a push or pull request. When you think about it, linting and running tests can rely on the same setup so why install 5 times? Furthermore, if packages didn't change since the last build, we could skip installation altogether.&lt;/p&gt;

&lt;p&gt;Let's take a look at how to &lt;strong&gt;cache&lt;/strong&gt; in GitHub Actions next.&lt;/p&gt;

&lt;h1&gt;
  
  
  4. I Want to Cache
&lt;/h1&gt;

&lt;p&gt;Here is where things began to fall apart for me. The documentation didn't make it clear that the way to cache &lt;em&gt;differs&lt;/em&gt; between &lt;code&gt;yarn&lt;/code&gt; and &lt;code&gt;npm&lt;/code&gt;. It also didn't show how to avoid &lt;code&gt;yarn install&lt;/code&gt; when the cache &lt;em&gt;is&lt;/em&gt; available and up-to-date. Hopefully, this section will save you from agony.&lt;/p&gt;

&lt;p&gt;To illustrate caching, I'll direct your attention to one job, say &lt;code&gt;test-partition-1&lt;/code&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;# File: .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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&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;test-partition-1&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 - Partition &lt;/span&gt;&lt;span class="m"&gt;1&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;${{ matrix.os }}&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;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;12.x&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out a copy of the repo&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use Node.js ${{ matrix.node-version }}&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-node@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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.node-version }}&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 dependencies&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install --frozen-lockfile&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;Test Ember app&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test --partition=1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We want to know how to update lines 22-23 so that the job does &lt;code&gt;yarn install&lt;/code&gt; only when necessary. The changes that we will make also apply to the other jobs.&lt;/p&gt;

&lt;p&gt;The idea is simple. First, &lt;code&gt;yarn&lt;/code&gt; keeps a &lt;strong&gt;global cache&lt;/strong&gt; that stores every package that you use. This way, it doesn't need to download the same package again. We want to cache that global cache. Second, from experience, we know that creating the &lt;code&gt;node_modules&lt;/code&gt; folder takes time. Let's cache that too! When the global cache or &lt;code&gt;node_modules&lt;/code&gt; folder is out of date, we will run &lt;code&gt;yarn install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The hard parts are digging documentation and scouring the web for examples. I'll save you the trouble. In the end, we get lines 22-48:&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;# File: .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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&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;test-partition-1&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 - Partition &lt;/span&gt;&lt;span class="m"&gt;1&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;${{ matrix.os }}&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;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;12.x&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out a copy of the repo&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use Node.js ${{ matrix.node-version }}&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-node@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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.node-version }}&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;Get Yarn cache path&lt;/span&gt;
              &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn-cache-dir-path&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "::set-output name=dir::$(yarn cache dir)"&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;Cache Yarn cache&lt;/span&gt;
              &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cache-yarn-cache&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/cache@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;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.yarn-cache-dir-path.outputs.dir }}&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;${{ runner.os }}-${{ matrix.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }}&lt;/span&gt;
                &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                  &lt;span class="s"&gt;${{ runner.os }}-${{ matrix.node-version }}-yarn-&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;Cache node_modules&lt;/span&gt;
              &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cache-node-modules&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/cache@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;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node_modules&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;${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}&lt;/span&gt;
                &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                  &lt;span class="s"&gt;${{ runner.os }}-${{ matrix.node-version }}-nodemodules-&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 dependencies&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install --frozen-lockfile&lt;/span&gt;
              &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                &lt;span class="s"&gt;steps.cache-yarn-cache.outputs.cache-hit != 'true' ||&lt;/span&gt;
                &lt;span class="s"&gt;steps.cache-node-modules.outputs.cache-hit != 'true'&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;Test Ember app&lt;/span&gt;
              &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test --partition=1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amid the changes, I want you to grasp just 3 things.&lt;/p&gt;

&lt;p&gt;First, the workflow needs to know where to find the global cache to cache it. We use &lt;code&gt;yarn cache dir&lt;/code&gt; to find the path (line 24) and pass it to the next step via &lt;code&gt;id&lt;/code&gt; (line 23) so that we don't hardcode a path that works for one OS but not others. (For &lt;code&gt;npm&lt;/code&gt;, the documentation showed &lt;code&gt;path: ~/.npm&lt;/code&gt;. It works in Linux and Mac, but not Windows.)&lt;/p&gt;

&lt;p&gt;Second, the workflow needs to know when it is okay to use a cache. The criterion will depend on what we're caching. For the global cache and &lt;code&gt;node_modules&lt;/code&gt; folder, we can be certain that it's okay to use the cache if &lt;code&gt;yarn.lock&lt;/code&gt; hasn't changed. &lt;code&gt;hashFiles()&lt;/code&gt; allows us to check for a file difference with efficiency and high confidence. We encode this criterion by including the hash in the cache's &lt;code&gt;key&lt;/code&gt; (lines 31 and 40).&lt;/p&gt;

&lt;p&gt;Finally, we can use &lt;code&gt;if&lt;/code&gt; to take a conditional step (line 46). The action, &lt;code&gt;actions/cache&lt;/code&gt;, returns a Boolean to indicate if it found a cache. As a result, we can tell the workflow to install dependencies if the &lt;code&gt;yarn.lock&lt;/code&gt; file changed.&lt;/p&gt;

&lt;p&gt;Thanks to caching, all jobs can now skip &lt;code&gt;yarn install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6-3VQYea--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_4_github_actions.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6-3VQYea--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_4_github_actions.png" alt="" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;
GitHub Actions can cache dependencies.



&lt;h1&gt;
  
  
  5. I Want to Take Percy Snapshots
&lt;/h1&gt;

&lt;p&gt;The last problem that we want to solve is taking Percy snapshots (visual regression tests) &lt;em&gt;in parallel&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  a. Setup
&lt;/h2&gt;

&lt;p&gt;If you haven't yet, make a new project in Percy. Link it to your GitHub repo by clicking on the Integrations tab. Finally, retrieve the project token, &lt;code&gt;PERCY_TOKEN&lt;/code&gt;, by switching to Project settings tab.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_Wo2XC9J--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5a_percy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_Wo2XC9J--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5a_percy.png" alt="" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
Link the Percy project to your GitHub repo.



&lt;p&gt;You can provide &lt;code&gt;PERCY_TOKEN&lt;/code&gt; to GitHub by visiting your repo and clicking on the Settings tab. Find the submenu called Secrets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XkijW1_s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5a_github.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XkijW1_s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5a_github.png" alt="" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
Pass the Percy token to GitHub.



&lt;p&gt;GitHub Actions can now access &lt;code&gt;PERCY_TOKEN&lt;/code&gt; and send Percy snapshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  b. First Attempt
&lt;/h2&gt;

&lt;p&gt;Integrating Percy with GitHub Actions isn't too difficult. Percy documented the how-to well and even provides an action, &lt;code&gt;percy/exec-action&lt;/code&gt;, to facilitate the workflow.&lt;/p&gt;

&lt;p&gt;Let's see what happens when we update the test step like this:&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;# File: .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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&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;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-1&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 - Partition &lt;/span&gt;&lt;span class="m"&gt;1&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;${{ matrix.os }}&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;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;12.x&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out a copy of the repo&lt;/span&gt;

            &lt;span class="s"&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;Test Ember app&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;percy/exec-action@v0.3.0&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;custom-command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test --partition=1&lt;/span&gt;
              &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;PERCY_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PERCY_TOKEN }}&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-4&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 - Partition &lt;/span&gt;&lt;span class="m"&gt;4&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;${{ matrix.os }}&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;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;12.x&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out a copy of the repo&lt;/span&gt;

            &lt;span class="s"&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;Test Ember app&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;percy/exec-action@v0.3.0&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;custom-command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test --partition=4&lt;/span&gt;
              &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;PERCY_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PERCY_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to change the &lt;code&gt;test&lt;/code&gt; script one last time. Let's prepend &lt;code&gt;percy exec --&lt;/code&gt;. It allows Percy to start and stop around the supplied command.&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;package&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&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;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;ember build --environment=production&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;lint:dependency&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;ember dependency-lint&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;lint:hbs&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;ember-template-lint .&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;lint:js&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;eslint .&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;start&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;ember serve&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;test&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;percy exec -- ember exam --query=nolint --split=4 --parallel=1&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we commit the changes, the tests for Ember will continue to pass. However, Percy will think that we made 4 builds rather than 1. It's hard to tell which of the four holds the "truth." Maybe none do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--STh2h1Op--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5b_percy_incorrect_setup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--STh2h1Op--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5b_percy_incorrect_setup.png" alt="" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;
Multiple Percy builds occur if we naively implement the parallel workflow.



&lt;p&gt;This problem occurs when we run tests in parallel. We need to tell Percy somehow that there are 4 jobs for testing and the snapshots belong to the same build.&lt;/p&gt;

&lt;h2&gt;
  
  
  c. Orchestrate
&lt;/h2&gt;

&lt;p&gt;Luckily, we can use Percy's environment variables to coordinate snapshots. Setting &lt;code&gt;PERCY_PARALLEL_TOTAL&lt;/code&gt;, the number of parallel build nodes, is easy in my case. It's always 4. But what about &lt;code&gt;PERCY_PARALLEL_NONCE&lt;/code&gt;, a unique identifier for the build?&lt;/p&gt;

&lt;p&gt;GitHub keeps track of two variables, &lt;code&gt;run_id&lt;/code&gt; and &lt;code&gt;run_number&lt;/code&gt;, for your repo. The former is a number for each run in the repo (e.g. 56424940, 57489786, 57500258), while the latter is a number for each run of a particular workflow in the repo (e.g. 44, 45, 46). Just to be safe, I combined the two to arrive at a nonce.&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;# File: .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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;PERCY_PARALLEL_NONCE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.run_id }}-${{ github.run_number }}&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;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-1&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 - Partition &lt;/span&gt;&lt;span class="m"&gt;1&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;${{ matrix.os }}&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;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;12.x&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out a copy of the repo&lt;/span&gt;

            &lt;span class="s"&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;Test Ember app&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;percy/exec-action@v0.3.0&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;custom-command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test --partition=1&lt;/span&gt;
              &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;PERCY_PARALLEL_NONCE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.PERCY_PARALLEL_NONCE }}&lt;/span&gt;
                &lt;span class="na"&gt;PERCY_PARALLEL_TOTAL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
                &lt;span class="na"&gt;PERCY_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PERCY_TOKEN }}&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;test-partition-4&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 - Partition &lt;/span&gt;&lt;span class="m"&gt;4&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;${{ matrix.os }}&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;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;12.x&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out a copy of the repo&lt;/span&gt;

            &lt;span class="s"&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;Test Ember app&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;percy/exec-action@v0.3.0&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;custom-command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test --partition=4&lt;/span&gt;
              &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;PERCY_PARALLEL_NONCE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.PERCY_PARALLEL_NONCE }}&lt;/span&gt;
                &lt;span class="na"&gt;PERCY_PARALLEL_TOTAL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
                &lt;span class="na"&gt;PERCY_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PERCY_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you introduce these environment variables, Percy will group the snapshots to a single build.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---LwdG_HW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5b_percy_correct_setup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---LwdG_HW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5b_percy_correct_setup.png" alt="" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;
A single Percy build occurs when we implement the parallel workflow right.



&lt;h1&gt;
  
  
  6. Conclusion
&lt;/h1&gt;

&lt;p&gt;Overall, I had a great time figuring out how to write a CI workflow for Ember apps in GitHub Actions. Writing code helped me understand better the steps involved in a CI. Not all was great, though. The documentation for caching can definitely use help with showing clear, exhaustive examples.&lt;/p&gt;

&lt;p&gt;At any rate, now, I can sit back and enjoy the benefits of linting and running tests with every commit. I'm looking forward to see what &lt;a href="https://ember-music.herokuapp.com/"&gt;Ember Music&lt;/a&gt; will turn into.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--o55fOyvm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_6_badges.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--o55fOyvm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_6_badges.png" alt="" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;
You made it! Give yourself some badges.



&lt;h1&gt;
  
  
  Notes
&lt;/h1&gt;

&lt;p&gt;You can find my CI workflow for Ember apps on GitHub Gist (&lt;a href="https://gist.github.com/ijlee2/2e7be877ceb60b9fa7c80298121d1024"&gt;yarn&lt;/a&gt;, &lt;a href="https://gist.github.com/ijlee2/030df226d02e32f6e4311591f58d87f8"&gt;npm&lt;/a&gt;). It works for all operating systems: Linux, Mac, and Windows.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;testem.js&lt;/code&gt;, you will see a reference to &lt;code&gt;process.env.CI&lt;/code&gt;:&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;// File: testem.js&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;test_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tests/index.html?hidepassed&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="na"&gt;browser_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="c1"&gt;// --no-sandbox is needed when running Chrome inside a container&lt;/span&gt;
                &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--no-sandbox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--headless&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="s1"&gt;--disable-dev-shm-usage&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="s1"&gt;--disable-software-rasterizer&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="s1"&gt;--mute-audio&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="s1"&gt;--remote-debugging-port=0&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="s1"&gt;--window-size=1440,900&lt;/span&gt;&lt;span class="dl"&gt;'&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="nb"&gt;Boolean&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not sure where &lt;code&gt;--no-sandbox&lt;/code&gt; gets used (this &lt;a href="https://www.google.com/googlebooks/chrome/med_26.html"&gt;comic&lt;/a&gt; explains &lt;strong&gt;sandbox&lt;/strong&gt;) and haven't found a need for it yet. If you need it for CI, please check the &lt;code&gt;ember-animated&lt;/code&gt; example below. It seems, at the job level, you can set the environment variable.&lt;/p&gt;

&lt;p&gt;I would like to know more about the history of and need for &lt;code&gt;--no-sandbox&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;If you want to learn more about GitHub Actions, Ember Exam, and Percy, I encourage you to visit these links:&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Actions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://help.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions"&gt;About Billing for GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.github.com/actions/configuring-and-managing-workflows/configuring-a-workflow"&gt;Configuring a Workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions"&gt;Using Node.js with GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.github.com/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows"&gt;Caching Dependencies to Speed Up Workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/actions/cache/blob/master/examples.md#node---npm"&gt;Cache Implementation for &lt;code&gt;npm&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/actions/cache/blob/master/examples.md#node---yarn"&gt;Cache Implementation for &lt;code&gt;yarn&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ember Exam
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ember-cli.com/ember-exam/docs"&gt;Quickstart&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Percy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.percy.io/docs/github-actions"&gt;GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.percy.io/docs/parallel-test-suites"&gt;Parallel Test Suites&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Workflow Examples
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/chart-parts/blob/master/.github/workflows/ci.yml"&gt;chart-parts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-animation/ember-animated/blob/master/.github/workflows/nodejs.yml"&gt;ember-animated&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/NullVoxPopuli/emberclear/blob/master/.github/workflows/frontend-tests.yml"&gt;ember-clear&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ember-cli/ember-cli-htmlbars/blob/master/.github/workflows/ci.yml"&gt;ember-cli-htmlbars&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/glimmerjs/glimmer.js/blob/master/.github/workflows/ci.yml"&gt;glimmer.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ember</category>
      <category>javascript</category>
      <category>ci</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
