<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Marcus Chukwuoma — marcdomain.dev]]></title><description><![CDATA[DevOps / Software Engineer. Practical guides on infrastructure as code, CI/CD, containers, Kubernetes and test automation.]]></description><link>http://github.com/dylang/node-rss</link><generator>GatsbyJS</generator><lastBuildDate>Mon, 09 Nov 2026 14:14:33 GMT</lastBuildDate><item><title><![CDATA[vaqum: compress, encrypt, diff and shred files from the command line]]></title><description><![CDATA[A single CLI to compress files with zstd or xz, encrypt them, verify them, diff folders and archives, find duplicates, search, and securely delete. Here's how to use each part.]]></description><link>https://marcdomain.dev/vaqum/</link><guid isPermaLink="false">https://marcdomain.dev/vaqum/</guid><pubDate>Sun, 23 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I kept reaching for five different tools to do one job: &lt;code class=&quot;language-text&quot;&gt;zip&lt;/code&gt; or &lt;code class=&quot;language-text&quot;&gt;tar&lt;/code&gt; to archive, &lt;code class=&quot;language-text&quot;&gt;gpg&lt;/code&gt; to
encrypt, &lt;code class=&quot;language-text&quot;&gt;diff&lt;/code&gt; to compare, &lt;code class=&quot;language-text&quot;&gt;fdupes&lt;/code&gt; to find duplicates, &lt;code class=&quot;language-text&quot;&gt;shred&lt;/code&gt; to delete. They all have different
flags, and not all of them exist on every OS.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/marcdomain/vaqum&quot;&gt;vaqum&lt;/a&gt; puts them in one command that works the same on
macOS, Linux and Windows. It&apos;s written in Rust, ships as a single binary, and at default settings
it&apos;s about &lt;strong&gt;5x faster than zip/gzip with output about 12% smaller&lt;/strong&gt;.&lt;/p&gt;
&lt;figure class=&quot;demo&quot;&gt;
  &lt;video src=&quot;/guides/vaqum-demo.mp4&quot; poster=&quot;/guides/vaqum-demo.jpg&quot; width=&quot;1000&quot; height=&quot;640&quot; autoplay muted loop playsinline controls preload=&quot;metadata&quot;&gt;&lt;/video&gt;
  &lt;figcaption&gt;The demo runs through every command below.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;what-it-does&quot;&gt;What it does&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compress files and folders, optionally encrypted&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vaqum compress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decompress &lt;code class=&quot;language-text&quot;&gt;.vaqum&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;.zip&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;.tar.gz&lt;/code&gt; archives&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vaqum decompress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inspect an archive, file or folder&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vaqum info&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compare files, folders or archives&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vaqum diff&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find (and hardlink) duplicate files&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vaqum dedupe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search names and contents&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vaqum search&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Securely delete&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vaqum shred&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saved defaults and profiles&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vaqum config&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell completions&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;vaqum completions&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;install&quot;&gt;Install&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; i &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; @marcdomain/vaqum                    &lt;span class=&quot;token comment&quot;&gt;# macOS, Linux, Windows&lt;/span&gt;
brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; marcdomain/vaqum/vaqum           &lt;span class=&quot;token comment&quot;&gt;# Homebrew&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/marcdomain/vaqum/main/install.sh &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sh&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;# macOS, Linux&lt;/span&gt;
irm https://raw.githubusercontent.com/marcdomain/vaqum/main/install.ps1 &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; iex        &lt;span class=&quot;token comment&quot;&gt;# Windows PowerShell&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The npm package pulls in a prebuilt binary for your platform, so there&apos;s nothing to compile.
Check it worked:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;compress-and-decompress&quot;&gt;Compress and decompress&lt;/h2&gt;
&lt;p&gt;Compress a file. &lt;code class=&quot;language-text&quot;&gt;-v&lt;/code&gt; shows the stats:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum compress report.txt &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;✔ Compressed &apos;report.txt&apos; -&gt; &apos;report.txt.vaqum&apos;
  algorithm:    zstd
  original:     12.61 kB
  compressed:   516 B (24.44x)
  output size:  599 B (21.05x)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;By default vaqum uses &lt;strong&gt;zstd at level 9&lt;/strong&gt;. Pick a level from 1 (fastest) to 22 (smallest) with
&lt;code class=&quot;language-text&quot;&gt;-l&lt;/code&gt;, or use &lt;code class=&quot;language-text&quot;&gt;--max&lt;/code&gt; to switch to &lt;strong&gt;xz (LZMA)&lt;/strong&gt;, which is slower and is usually smaller on larger
inputs. On a tiny file like this one, zstd actually wins, so it&apos;s worth checking with &lt;code class=&quot;language-text&quot;&gt;--dry-run&lt;/code&gt;
first:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum compress report.txt --dry-run          &lt;span class=&quot;token comment&quot;&gt;# estimate without writing anything&lt;/span&gt;
vaqum compress report.txt &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;19&lt;/span&gt;              &lt;span class=&quot;token comment&quot;&gt;# higher zstd level&lt;/span&gt;
vaqum compress report.txt &lt;span class=&quot;token parameter variable&quot;&gt;--max&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; report-max.vaqum&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Decompress, and check the result matches the original&apos;s SHA-256 checksum with &lt;code class=&quot;language-text&quot;&gt;--verify&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum decompress report.txt.vaqum &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; restored &lt;span class=&quot;token parameter variable&quot;&gt;--verify&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;✔ verified checksum (48a077133b4b4e0541da861082275b948089ef3d6f3c33085a858095d34819c2)
✔ Decompressed -&gt; restored&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;folders-and-bundles&quot;&gt;Folders and bundles&lt;/h3&gt;
&lt;p&gt;Compress a folder with &lt;code class=&quot;language-text&quot;&gt;-r&lt;/code&gt;, or several paths into one archive with &lt;code class=&quot;language-text&quot;&gt;-o&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum compress project &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt;
vaqum compress report.txt notes-v1.txt notes-v2.txt &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; bundle.vaqum&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Skip paths with &lt;code class=&quot;language-text&quot;&gt;--exclude&lt;/code&gt; (repeatable). End a pattern with &lt;code class=&quot;language-text&quot;&gt;/&lt;/code&gt; for a folder; a name without &lt;code class=&quot;language-text&quot;&gt;/&lt;/code&gt;
matches at any depth:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum compress webapp &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--exclude&lt;/span&gt; node_modules/ &lt;span class=&quot;token parameter variable&quot;&gt;--exclude&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.log&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; webapp.vaqum&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or put the patterns in a &lt;code class=&quot;language-text&quot;&gt;.vaqumignore&lt;/code&gt; file at the folder&apos;s root, one per line, like a
&lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;node_modules/
dist/
*.log&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;deduplication&quot;&gt;Deduplication&lt;/h3&gt;
&lt;p&gt;If a folder has repeated files, &lt;code class=&quot;language-text&quot;&gt;--dedup&lt;/code&gt; stores each unique file once:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum compress photos &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--dedup&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;  new img1.txt  (12.61 kB)
  dup img2.txt  (12.61 kB)
  new img3.txt  (26 B)
✔ Compressed &apos;photos&apos; -&gt; &apos;photos.vaqum&apos;
  algorithm:    zstd + dedup&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Compression uses every CPU core by default; limit it with &lt;code class=&quot;language-text&quot;&gt;-t &amp;lt;threads&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;encrypt&quot;&gt;Encrypt&lt;/h2&gt;
&lt;p&gt;Add &lt;code class=&quot;language-text&quot;&gt;-e&lt;/code&gt; to encrypt with a password, or &lt;code class=&quot;language-text&quot;&gt;--key-file&lt;/code&gt; to use a file&apos;s bytes as the key:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum compress secret.txt &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; secret.vaqum
vaqum compress secret.txt --key-file key.bin &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; secret.vaqum&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;✔ Compressed and encrypted &apos;secret.txt&apos; -&gt; &apos;secret.vaqum&apos;
  encryption:   ChaCha20-Poly1305, Argon2id KDF&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The data is encrypted with &lt;strong&gt;ChaCha20-Poly1305&lt;/strong&gt;. The key is derived from your password with
&lt;strong&gt;Argon2id&lt;/strong&gt;, and only the salt is stored in the archive, never the key. &lt;code class=&quot;language-text&quot;&gt;decompress&lt;/code&gt; detects
encryption by itself and asks for the password (or takes &lt;code class=&quot;language-text&quot;&gt;--key-file&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id=&quot;open-zip-and-targz-files-too&quot;&gt;Open zip and tar.gz files too&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;decompress&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;info&lt;/code&gt; also read &lt;code class=&quot;language-text&quot;&gt;.zip&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;.tar.gz&lt;/code&gt;/&lt;code class=&quot;language-text&quot;&gt;.tgz&lt;/code&gt;, detected by content rather than
file extension, so you don&apos;t need another tool to open something a teammate sent:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum info shared.zip
vaqum decompress shared.tar.gz &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; shared-restored&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is one-way: vaqum only &lt;em&gt;writes&lt;/em&gt; its own format.&lt;/p&gt;
&lt;h3 id=&quot;protection-from-decompression-bombs&quot;&gt;Protection from decompression bombs&lt;/h3&gt;
&lt;p&gt;A small archive can be crafted to expand into terabytes. &lt;code class=&quot;language-text&quot;&gt;decompress&lt;/code&gt; refuses archives that claim
an expansion ratio above 1000:1, or that wouldn&apos;t fit on the disk. Change the limit with
&lt;code class=&quot;language-text&quot;&gt;--max-ratio&lt;/code&gt;, or skip the check with &lt;code class=&quot;language-text&quot;&gt;--force&lt;/code&gt; when you trust the source.&lt;/p&gt;
&lt;h2 id=&quot;inspect&quot;&gt;Inspect&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;info&lt;/code&gt; works on archives without fully decompressing them, and on plain files and folders:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum info report.txt.vaqum&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;report.txt.vaqum
  type:         single file
  name:         report.txt
  algorithm:    zstd
  original:     12.61 kB
  compressed:   516 B (24.44x)
  on disk:      599 B
  checksum:     sha256:48a077133b4b4e0541da861082275b948089ef3d6f3c33085a858095d34819c2
  encrypted:    no&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;diff-files-folders-and-archives&quot;&gt;Diff files, folders and archives&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;diff&lt;/code&gt; compares any mix of files, folders and &lt;code class=&quot;language-text&quot;&gt;.vaqum&lt;/code&gt; archives. That makes it easy to check a
backup against the live copy without unpacking it first.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum &lt;span class=&quot;token function&quot;&gt;diff&lt;/span&gt; project-backup project &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;comparing &apos;project-backup&apos; vs &apos;project&apos;:
  1 unchanged, 1 added, 0 removed, 1 modified
  + src/util.rs  (19 B)
  ~ src/main.rs
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,3 @@
 fn main() {
-    println!(&quot;v1&quot;);
+    println!(&quot;v2&quot;);
 }&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Three other ways to view a diff:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum &lt;span class=&quot;token function&quot;&gt;diff&lt;/span&gt; a.txt b.txt &lt;span class=&quot;token parameter variable&quot;&gt;--html&lt;/span&gt; diff.html   &lt;span class=&quot;token comment&quot;&gt;# self-contained, offline, side-by-side HTML report&lt;/span&gt;
vaqum &lt;span class=&quot;token function&quot;&gt;diff&lt;/span&gt; a.txt b.txt &lt;span class=&quot;token parameter variable&quot;&gt;--open&lt;/span&gt;             &lt;span class=&quot;token comment&quot;&gt;# same report, opened in your browser&lt;/span&gt;
vaqum &lt;span class=&quot;token function&quot;&gt;diff&lt;/span&gt; old/ new/ &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt;                   &lt;span class=&quot;token comment&quot;&gt;# VS Code&apos;s split view (code --diff), one tab per changed file&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Set &lt;code class=&quot;language-text&quot;&gt;VAQUM_DIFF_EDITOR&lt;/code&gt; to use another editor. When one side is a &lt;code class=&quot;language-text&quot;&gt;.vaqum&lt;/code&gt; archive, it&apos;s unpacked
to a scratch copy first, so edits there don&apos;t change the archive.&lt;/p&gt;
&lt;h2 id=&quot;find-duplicates&quot;&gt;Find duplicates&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;dedupe&lt;/code&gt; hashes every file in a folder and groups identical ones:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum dedupe photos &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;photos
  files scanned:     3 (25.24 kB)
  duplicate groups:  1 (1 redundant file)
  reclaimable:       12.61 kB

  sha256:48a07713…819c2 (12.61 kB, 2 copies)
    img1.txt
    img2.txt&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Add &lt;code class=&quot;language-text&quot;&gt;--link&lt;/code&gt; to replace duplicates with hardlinks to the first copy. That frees the space without
deleting any path. Preview it first with &lt;code class=&quot;language-text&quot;&gt;--link --dry-run&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;search&quot;&gt;Search&lt;/h2&gt;
&lt;p&gt;Search file names and contents at once, recursively:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum search TODO app&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;name     app/TODO-cleanup
content  app/main.rs:2: // TODO: wire up config
content  app/util.rs:1: // TODO: add tests

3 matches (1 file by name, 2 lines of content)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Use &lt;code class=&quot;language-text&quot;&gt;-i&lt;/code&gt; for case-insensitive, &lt;code class=&quot;language-text&quot;&gt;-E&lt;/code&gt; for a regular expression, and &lt;code class=&quot;language-text&quot;&gt;--names-only&lt;/code&gt; or
&lt;code class=&quot;language-text&quot;&gt;--content-only&lt;/code&gt; to narrow it down:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum search &lt;span class=&quot;token string&quot;&gt;&apos;TODO: \w+&apos;&lt;/span&gt; app &lt;span class=&quot;token parameter variable&quot;&gt;-E&lt;/span&gt; --content-only&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;shred&quot;&gt;Shred&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;shred&lt;/code&gt; overwrites a file before deleting it, so it can&apos;t be recovered from the disk. It asks you
to confirm first (skip that with &lt;code class=&quot;language-text&quot;&gt;-y&lt;/code&gt; in scripts):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum shred secret.txt
vaqum shred old-keys/ &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;# a folder, with 7 overwrite passes&lt;/span&gt;
vaqum shred old-keys/ &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; --dry-run &lt;span class=&quot;token comment&quot;&gt;# list what would be destroyed&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;⚠  This will permanently and irrecoverably destroy:
    secret.txt (28 B)
✔ Shredded (3 passes, DoD 5220.22-M-style overwrite)
  note: on SSDs, overwrite passes are best-effort (wear-leveling/TRIM) — not forensic-grade.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As the note says, SSDs move data around internally, so overwriting is best-effort there. For
data that must never be recoverable on an SSD, use full-disk encryption from the start.&lt;/p&gt;
&lt;h2 id=&quot;save-your-defaults&quot;&gt;Save your defaults&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;vaqum config init&lt;/code&gt; writes a starter file at &lt;code class=&quot;language-text&quot;&gt;~/.config/vaqum/config.toml&lt;/code&gt;
(&lt;code class=&quot;language-text&quot;&gt;%APPDATA%\vaqum\config.toml&lt;/code&gt; on Windows):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;toml&quot;&gt;&lt;pre class=&quot;language-toml&quot;&gt;&lt;code class=&quot;language-toml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;defaults&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;           &lt;span class=&quot;token comment&quot;&gt;# every compress&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;exclude&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;*.log&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;compress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;           &lt;span class=&quot;token comment&quot;&gt;# overrides [defaults] for compress&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;profile.release&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;# used with --profile release&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;max&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum compress dist &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--profile&lt;/span&gt; release &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; release.vaqum
vaqum config show    &lt;span class=&quot;token comment&quot;&gt;# what&apos;s in effect&lt;/span&gt;
vaqum config path    &lt;span class=&quot;token comment&quot;&gt;# where the file is&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Flags on the command line always win over the file.&lt;/p&gt;
&lt;h2 id=&quot;shell-completions&quot;&gt;Shell completions&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum completions &lt;span class=&quot;token function&quot;&gt;zsh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--install&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;# also bash, fish, powershell, elvish&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;--install&lt;/code&gt; writes the script to your shell&apos;s standard completions folder and tells you if one line
needs adding to your shell config. It never edits your config itself.&lt;/p&gt;
&lt;h2 id=&quot;scripts-and-ci&quot;&gt;Scripts and CI&lt;/h2&gt;
&lt;p&gt;Progress bars show on interactive terminals and hide themselves when output is piped. &lt;code class=&quot;language-text&quot;&gt;-q&lt;/code&gt; hides
them explicitly. Combined with &lt;code class=&quot;language-text&quot;&gt;-y&lt;/code&gt; on &lt;code class=&quot;language-text&quot;&gt;shred&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;--key-file&lt;/code&gt; for encryption, every command runs
without prompts:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;vaqum compress build/ &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-q&lt;/span&gt; --key-file &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$KEY_FILE&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;build-&lt;span class=&quot;token variable&quot;&gt;$GITHUB_SHA&lt;/span&gt;.vaqum&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;links&quot;&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/marcdomain/vaqum&quot;&gt;Source on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/@marcdomain/vaqum&quot;&gt;npm package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code class=&quot;language-text&quot;&gt;vaqum &amp;lt;command&gt; --help&lt;/code&gt; for every flag&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Publish multi-arch base images to any registry with one GitHub Action]]></title><description><![CDATA[multiarch-image-pusher copies a base image into your own registry as an amd64/arm64 manifest, optionally with its OS packages updated first. Works with ECR, GHCR, GitLab, Docker Hub and others.]]></description><link>https://marcdomain.dev/multiarch-image-pusher/</link><guid isPermaLink="false">https://marcdomain.dev/multiarch-image-pusher/</guid><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most teams build on top of public base images like &lt;code class=&quot;language-text&quot;&gt;python:3.12-slim&lt;/code&gt; or
&lt;code class=&quot;language-text&quot;&gt;amazon/aws-lambda-python:3.12&lt;/code&gt;. Pulling them straight from Docker Hub in every build has
drawbacks: rate limits, an outside dependency in every pipeline, and no control over when the base
changes. And with Apple Silicon laptops and Graviton servers, you need both &lt;strong&gt;amd64&lt;/strong&gt; and &lt;strong&gt;arm64&lt;/strong&gt;
images.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/marcdomain/multiarch-image-pusher&quot;&gt;multiarch-image-pusher&lt;/a&gt; is a GitHub Action
that copies a base image into &lt;strong&gt;your&lt;/strong&gt; registry as a multi-arch image, in one step. It can also
rebuild it with the latest OS security updates on the way.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 820px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.09756097560976%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAACFUlEQVR42mWSS2/bMBCEdSximRSfIqmnLVu2kzZIixZxgxSOUzcIkKCHIqf+//8xXTJqDu1hQEkUv53ZZeZXGs15QL1zqLYWYSStLfzSoOw1TEuqFaSXKIwAVwWYiOKYRxUMOSexeVJmFwrd+wbNtkK9cXBLDd0KyKqACBzM0o8qBzc8AZkskl5hJD4B+QSMDu4ev+H0fMTt6Stu7ve4PnzB8fGA/eEax4cDHp5+YLVbY0aHuBT/u5tgszkBZZC4vb/Bz5dnnJ6+p/Xl9y/cEci1Du3QYjkOsKFMkBg3OSPYv3Fn8xxZYSmekzCVha1L+C6grB20NwRgydW7/AxnLEceIVPUN2fJ3V8oQxb7kRqdesMTZD5FKpSAdhau9qQA422KG2HzgpwyQZIklcS4RhYPCiOhSg1pFa0G0ihwLeBWPWzlU+Td5QXqvoWmd2E9pKZb0Pi0X1IxGyoUoiIgxYjVV9sRw2aNxXpAveygF01yXRA4OeacBqLpkIVu6L+PI5qNxOZTj+6iRD16KDMgi9ljNGk1ykC9I4dCSTAtYQlq6duSioznWzR9A+Ur0opM9KiHDv1I4GFA6FbQZocsTiY2NjqNz2f57G1lsZCJhQJCQ3FLcmFrCNlBygFabaH0DsZcwtor2PLqFchoakW8X3F6dJdm0xWYJXicYGy+pv1AsIb6tyTQCK23MOUHlARy7jO82+MPFY8ZetfZU8AAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/744c1e6e0d82e7a76d89a49432654141/ac387/cover.webp 205w,
/static/744c1e6e0d82e7a76d89a49432654141/0cb6c/cover.webp 410w,
/static/744c1e6e0d82e7a76d89a49432654141/b1f58/cover.webp 820w,
/static/744c1e6e0d82e7a76d89a49432654141/fc6cc/cover.webp 1230w,
/static/744c1e6e0d82e7a76d89a49432654141/af3f0/cover.webp 1280w&quot;
              sizes=&quot;(max-width: 820px) 100vw, 820px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/744c1e6e0d82e7a76d89a49432654141/85860/cover.png 205w,
/static/744c1e6e0d82e7a76d89a49432654141/d68e4/cover.png 410w,
/static/744c1e6e0d82e7a76d89a49432654141/9f82e/cover.png 820w,
/static/744c1e6e0d82e7a76d89a49432654141/0d0e4/cover.png 1230w,
/static/744c1e6e0d82e7a76d89a49432654141/21b4d/cover.png 1280w&quot;
            sizes=&quot;(max-width: 820px) 100vw, 820px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/744c1e6e0d82e7a76d89a49432654141/9f82e/cover.png&quot;
            alt=&quot;multiarch-image-pusher&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;em&gt;One base image in, per-architecture tags and a combined manifest out&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-you-get&quot;&gt;What you get&lt;/h2&gt;
&lt;p&gt;For each run, the action publishes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one tag per platform: &lt;code class=&quot;language-text&quot;&gt;:&amp;lt;tag&gt;-amd64&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;:&amp;lt;tag&gt;-arm64&lt;/code&gt; by default;&lt;/li&gt;
&lt;li&gt;the combined &lt;code class=&quot;language-text&quot;&gt;:&amp;lt;tag&gt;&lt;/code&gt;, a &lt;strong&gt;manifest list&lt;/strong&gt; that points to all of them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When someone runs &lt;code class=&quot;language-text&quot;&gt;docker pull my-registry/python-base:3.12-slim&lt;/code&gt;, Docker picks the right
architecture from the manifest list automatically.&lt;/p&gt;
&lt;p&gt;It works with any registry: Amazon ECR, GitHub Container Registry, GitLab Container Registry,
Docker Hub, or a generic registry.&lt;/p&gt;
&lt;h2 id=&quot;two-modes&quot;&gt;Two modes&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code class=&quot;language-text&quot;&gt;update-libraries&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Use it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;false&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;The image is copied as-is, registry to registry&lt;/td&gt;
&lt;td&gt;You want a mirror of the exact upstream image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Each platform is rebuilt with its OS packages updated, then pushed&lt;/td&gt;
&lt;td&gt;You want the latest security fixes without waiting for upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;When updating, the action detects the package manager in the image (dnf, microdnf, yum, apt-get,
apk or zypper), so the same step works for Debian, Ubuntu, Alpine, Amazon Linux, RHEL-family and
SUSE images. If it can&apos;t find one, it fails with a clear error instead of skipping the update.&lt;/p&gt;
&lt;h2 id=&quot;inputs&quot;&gt;Inputs&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;base-image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Image to copy, e.g. &lt;code class=&quot;language-text&quot;&gt;python:3.12-slim&lt;/code&gt;. The runner must be able to pull it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;dest-registry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Registry hostname, e.g. &lt;code class=&quot;language-text&quot;&gt;ghcr.io&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;dest-image-repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Repository in that registry, e.g. &lt;code class=&quot;language-text&quot;&gt;my-org/python-base&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;dest-image-tag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tag to publish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;update-libraries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;true&lt;/code&gt; to rebuild with OS packages updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;platforms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;linux/amd64,linux/arm64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Comma-separated buildx platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;buildkit-image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Custom BuildKit image (e.g. a private mirror), used when &lt;code class=&quot;language-text&quot;&gt;update-libraries&lt;/code&gt; is &lt;code class=&quot;language-text&quot;&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The action doesn&apos;t log in to any registry.&lt;/strong&gt; Add a login step before it for the destination, and
for the source too if it&apos;s private. That keeps your credentials in your own workflow.&lt;/p&gt;
&lt;h2 id=&quot;example-copy-to-ghcr-as-is&quot;&gt;Example: copy to GHCR as-is&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Publish base image

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
  &lt;span class=&quot;token key atrule&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Log in to GHCR
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; echo &quot;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.GITHUB_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&quot; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; docker login ghcr.io &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;username $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; github.actor &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;password&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stdin

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; marcdomain/multiarch&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pusher@v1
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;base-image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; python&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;3.12&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;slim
          &lt;span class=&quot;token key atrule&quot;&gt;dest-registry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io
          &lt;span class=&quot;token key atrule&quot;&gt;dest-image-repo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;org/python&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;base
          &lt;span class=&quot;token key atrule&quot;&gt;dest-image-tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;3.12-slim&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This publishes &lt;code class=&quot;language-text&quot;&gt;ghcr.io/my-org/python-base:3.12-slim-amd64&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;:3.12-slim-arm64&lt;/code&gt;, and the combined
&lt;code class=&quot;language-text&quot;&gt;:3.12-slim&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;example-ecr-with-os-packages-updated-every-week&quot;&gt;Example: ECR, with OS packages updated, every week&lt;/h2&gt;
&lt;p&gt;Use GitHub&apos;s OIDC to assume an AWS role (no long-lived keys), and a schedule so the image picks up
security fixes regularly:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Refresh Lambda base image

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;schedule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;cron&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0 5 * * 1&quot;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;# Mondays at 05:00 UTC&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;id-token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;actions/configure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;credentials@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;role-to-assume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; arn&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;aws&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;iam&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;123456789012&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;role/github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;actions&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ecr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;push
          &lt;span class=&quot;token key atrule&quot;&gt;aws-region&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; eu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;west&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Log in to Amazon ECR
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aws ecr get&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;login&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;password &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;region eu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;west&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;1 &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; docker login &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;username AWS &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;password&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stdin 123456789012.dkr.ecr.eu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;west&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;1.amazonaws.com

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; marcdomain/multiarch&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pusher@v1
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;base-image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; amazon/aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;lambda&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;python&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3.12&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;dest-registry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 123456789012.dkr.ecr.eu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;west&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;1.amazonaws.com
          &lt;span class=&quot;token key atrule&quot;&gt;dest-image-repo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;lambda
          &lt;span class=&quot;token key atrule&quot;&gt;dest-image-tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;3.12&apos;&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;update-libraries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;true&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Your Dockerfiles then build &lt;code class=&quot;language-text&quot;&gt;FROM 123456789012.dkr.ecr.eu-west-1.amazonaws.com/aws-lambda:3.12&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;example-docker-hub&quot;&gt;Example: Docker Hub&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Log in to Docker Hub
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; echo &quot;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.DOCKERHUB_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&quot; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; docker login docker.io &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;username $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.DOCKERHUB_USERNAME &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;password&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stdin

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; marcdomain/multiarch&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pusher@v1
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;base-image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; alpine&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3.20&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;dest-registry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker.io
          &lt;span class=&quot;token key atrule&quot;&gt;dest-image-repo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;org/alpine&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;base
          &lt;span class=&quot;token key atrule&quot;&gt;dest-image-tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;3.20&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;example-more-platforms&quot;&gt;Example: more platforms&lt;/h2&gt;
&lt;p&gt;Add any platform the base image publishes, using buildx syntax. For example, 32-bit ARM for
Raspberry Pi-class hardware:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; marcdomain/multiarch&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pusher@v1
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;base-image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;20&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;bullseye
          &lt;span class=&quot;token key atrule&quot;&gt;dest-registry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; registry.gitlab.com
          &lt;span class=&quot;token key atrule&quot;&gt;dest-image-repo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;group/my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;project/node&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;base
          &lt;span class=&quot;token key atrule&quot;&gt;dest-image-tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;20-bullseye&apos;&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;platforms&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; linux/amd64&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;linux/arm64&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;linux/arm/v7&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That adds a &lt;code class=&quot;language-text&quot;&gt;:20-bullseye-arm-v7&lt;/code&gt; tag to the manifest.&lt;/p&gt;
&lt;h2 id=&quot;example-private-source-image&quot;&gt;Example: private source image&lt;/h2&gt;
&lt;p&gt;Log in to both registries first:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Log in to source registry
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; echo &quot;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.SOURCE_REGISTRY_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&quot; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; docker login some&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;private&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;registry.example.com &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;username ci &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;password&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stdin

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Log in to destination registry
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; echo &quot;$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.DEST_REGISTRY_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&quot; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; docker login registry.gitlab.com &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;username ci &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;password&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stdin

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; marcdomain/multiarch&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pusher@v1
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;base-image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; some&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;private&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;registry.example.com/team/base&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1.0&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;dest-registry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; registry.gitlab.com
          &lt;span class=&quot;token key atrule&quot;&gt;dest-image-repo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;group/my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;project/node&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;base
          &lt;span class=&quot;token key atrule&quot;&gt;dest-image-tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;1.0&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;example-buildkit-from-a-private-mirror&quot;&gt;Example: BuildKit from a private mirror&lt;/h2&gt;
&lt;p&gt;Rebuilding (&lt;code class=&quot;language-text&quot;&gt;update-libraries: &apos;true&apos;&lt;/code&gt;) uses BuildKit, which normally comes from Docker Hub. On
rate-limited or locked-down runners, point it at your own copy:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; marcdomain/multiarch&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pusher@v1
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;base-image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; amazon/aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;lambda&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;python&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3.12&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;dest-registry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 123456789012.dkr.ecr.eu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;west&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;1.amazonaws.com
          &lt;span class=&quot;token key atrule&quot;&gt;dest-image-repo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;lambda
          &lt;span class=&quot;token key atrule&quot;&gt;update-libraries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;true&apos;&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;buildkit-image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 123456789012.dkr.ecr.eu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;west&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;1.amazonaws.com/mirror/buildkit&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Copy as-is&lt;/strong&gt; (&lt;code class=&quot;language-text&quot;&gt;update-libraries: &apos;false&apos;&lt;/code&gt;): for each platform, the action reads the base image&apos;s
manifest list, finds the entry for that architecture, and copies it to &lt;code class=&quot;language-text&quot;&gt;:&amp;lt;tag&gt;-&amp;lt;arch&gt;&lt;/code&gt; with
&lt;code class=&quot;language-text&quot;&gt;docker buildx imagetools create&lt;/code&gt;. This happens registry to registry, without pulling layers onto
the runner, so it&apos;s fast. Then it creates the combined &lt;code class=&quot;language-text&quot;&gt;:&amp;lt;tag&gt;&lt;/code&gt; from those tags.&lt;/p&gt;
&lt;p&gt;A detail that&apos;s easy to get wrong: &lt;code class=&quot;language-text&quot;&gt;linux/arm64&lt;/code&gt; in buildx syntax doesn&apos;t name a variant, but
registries record it as &lt;code class=&quot;language-text&quot;&gt;arm64/v8&lt;/code&gt; (and &lt;code class=&quot;language-text&quot;&gt;linux/arm&lt;/code&gt; as &lt;code class=&quot;language-text&quot;&gt;arm/v7&lt;/code&gt;). The action fills in those
defaults so the lookup matches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rebuild with updates&lt;/strong&gt; (&lt;code class=&quot;language-text&quot;&gt;update-libraries: &apos;true&apos;&lt;/code&gt;): the action sets up QEMU and buildx, writes a
small Dockerfile per platform that runs the detected package manager&apos;s update command (for example
&lt;code class=&quot;language-text&quot;&gt;apt-get update &amp;amp;&amp;amp; apt-get upgrade -y&lt;/code&gt; or &lt;code class=&quot;language-text&quot;&gt;apk upgrade&lt;/code&gt;), cleans the package cache, builds and
pushes each platform, then creates and pushes the manifest list with &lt;code class=&quot;language-text&quot;&gt;docker manifest&lt;/code&gt;. For Amazon
Linux, it also points the release version at &lt;code class=&quot;language-text&quot;&gt;latest&lt;/code&gt; so updates come from the newest repository.&lt;/p&gt;
&lt;h2 id=&quot;check-the-result&quot;&gt;Check the result&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; buildx imagetools inspect ghcr.io/my-org/python-base:3.12-slim&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You should see one entry per platform, for example &lt;code class=&quot;language-text&quot;&gt;linux/amd64&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;linux/arm64&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;good-to-know&quot;&gt;Good to know&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The base image must already publish every platform you ask for. If one is missing, the copy
fails with &lt;code class=&quot;language-text&quot;&gt;No manifest entry for platform … found in …&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rebuilding under QEMU emulation is slower than copying, especially for non-native platforms.
Copying is the default for that reason.&lt;/li&gt;
&lt;li&gt;Pin the action to a release tag (&lt;code class=&quot;language-text&quot;&gt;@v1&lt;/code&gt;, or a specific one like &lt;code class=&quot;language-text&quot;&gt;@v1.1.3&lt;/code&gt;) in production
workflows.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;links&quot;&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/marcdomain/multiarch-image-pusher&quot;&gt;Source on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[IGNOREIT: configure your ignore list once, and it's applied in every repo]]></title><description><![CDATA[A VS Code extension that makes sure the files you choose (.env files, keys, build folders, anything) are always in .gitignore, in every project you open. It can also keep .env.example in sync.]]></description><link>https://marcdomain.dev/ignoreit/</link><guid isPermaLink="false">https://marcdomain.dev/ignoreit/</guid><pubDate>Thu, 23 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every project ends up with files that should never be committed: &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; files and private keys,
but also &lt;code class=&quot;language-text&quot;&gt;node_modules&lt;/code&gt;, build output, test coverage, Terraform state, editor and OS files. Each new
repo needs the same &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; lines again, and it only takes one forgotten line and one
&lt;code class=&quot;language-text&quot;&gt;git add .&lt;/code&gt; for them to land in git.&lt;/p&gt;
&lt;p&gt;For build output that&apos;s just noise in your history. For secrets it&apos;s worse: deleting the file
afterwards doesn&apos;t help, because it stays in git history, every clone already has it, and bots scan
public repositories for keys within minutes. The only real fix then is to rotate every secret in
it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=marcdomain.ignoreit&quot;&gt;IGNOREIT&lt;/a&gt; removes that
step. &lt;strong&gt;You configure your list of files and folders once, in VS Code.&lt;/strong&gt; From then on, in every
project you open, any file or folder on that list is added to &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; as soon as it shows up,
before you get the chance to commit it.&lt;/p&gt;
&lt;p&gt;It&apos;s free, open source, and has been on the VS Code Marketplace since 2020, with 1,800+ installs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; install it, then list what you never want in git, for example
&lt;code class=&quot;language-text&quot;&gt;&quot;ignoreit.array&quot;: [&quot;.env&quot;, &quot;*.pem&quot;, &quot;node_modules&quot;, &quot;coverage&quot;]&lt;/code&gt;. From then on, those files and
folders are always ignored, in every repo you open.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure class=&quot;demo&quot;&gt;
  &lt;video src=&quot;/guides/ignoreit-demo.mp4&quot; poster=&quot;/guides/ignoreit-demo.jpg&quot; width=&quot;1920&quot; height=&quot;1200&quot; autoplay muted loop playsinline controls preload=&quot;metadata&quot;&gt;&lt;/video&gt;
  &lt;figcaption&gt;The whole flow in VS Code: set your list once, then .env, node_modules, a new key and coverage are ignored as they appear, .env.example stays in sync, and git status shows no secrets. Every file and notification matches what the extension produces.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;what-it-does&quot;&gt;What it does&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Applies your list everywhere.&lt;/strong&gt; One list in your VS Code settings covers every project. When a
file or folder from it exists in the project, it&apos;s added to &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;. No &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; yet?
One is created.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keeps watching.&lt;/strong&gt; Files that appear later, like a &lt;code class=&quot;language-text&quot;&gt;coverage&lt;/code&gt; folder after your first test run
or a key you download, are added as soon as they show up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leaves your own entries alone.&lt;/strong&gt; New lines are appended at the end. Anything you wrote by hand
stays where it is, and an entry that&apos;s already covered isn&apos;t added again.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Understands wildcards.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;*.pem&lt;/code&gt; covers &lt;code class=&quot;language-text&quot;&gt;server.pem&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;client.pem&lt;/code&gt;. Dotfiles like &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt;
match too.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keeps &lt;code class=&quot;language-text&quot;&gt;.env.example&lt;/code&gt; in sync (optional).&lt;/strong&gt; It copies the variable &lt;em&gt;names&lt;/em&gt; from &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; into
&lt;code class=&quot;language-text&quot;&gt;.env.example&lt;/code&gt;, without the values, so teammates know what to set.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tells you what it did.&lt;/strong&gt; A notification such as &lt;code class=&quot;language-text&quot;&gt;.env, *.pem added to .gitignore&lt;/code&gt; appears each
time it changes the file.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;install&quot;&gt;Install&lt;/h2&gt;
&lt;p&gt;In VS Code, open the Extensions view (&lt;strong&gt;Cmd/Ctrl + Shift + X&lt;/strong&gt;), search for &lt;strong&gt;ignoreit&lt;/strong&gt;, and
click &lt;strong&gt;Install&lt;/strong&gt;. Or from a terminal:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;code --install-extension marcdomain.ignoreit&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It needs nothing else. It works in any folder that is a git repository, meaning the folder you
open has a &lt;code class=&quot;language-text&quot;&gt;.git&lt;/code&gt; folder at its root.&lt;/p&gt;
&lt;h2 id=&quot;choose-what-to-ignore&quot;&gt;Choose what to ignore&lt;/h2&gt;
&lt;p&gt;Open Settings (&lt;strong&gt;Cmd/Ctrl + ,&lt;/strong&gt;) and search for &lt;strong&gt;ignoreit&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 820px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 44.390243902439025%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABRUlEQVR42o1S207CQBTcykVuKo3IreVmuy0VSsFWrKGIQQ3GRHzyE0x88CNM/PBx91gaeOvDZOZsT2Znew4r96fQvBVqwxDaJEK2OQSrmWAX/J8FFKGVhp0K7OTyGvrsgUw7xPdojBbigjviphuhygOwupXOMK+7UPkt8pqLXHsseETIta8SnRU6dcKc7uB8FECptsBUnZKwOo/ZoqcrKdORYdN8xPvmF9vnHwT+J1jDwpH4jxJpTQ4M1d4CYfiF5fIb9vhDDMEQQzhMtrsggTzbP481GWZaNjItE5n6AMe6jXLfQ6nrEhf0cTxtfjh5yXtbsNNyG8SUfRjzDXj4gslqCyd6FfUT3NUb+sEaxa6HU8NPcGb6kJtRGcyolro8mJIu9jyZ0EGhMxGNAX2kJtFc5TdUS0itWvOEd72ylroS61LPwx/7bu9N0SOIuwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/55e2e78887e405bfb982ec62e9cb61d3/ac387/ignoreit-settings.webp 205w,
/static/55e2e78887e405bfb982ec62e9cb61d3/0cb6c/ignoreit-settings.webp 410w,
/static/55e2e78887e405bfb982ec62e9cb61d3/b1f58/ignoreit-settings.webp 820w,
/static/55e2e78887e405bfb982ec62e9cb61d3/fc6cc/ignoreit-settings.webp 1230w,
/static/55e2e78887e405bfb982ec62e9cb61d3/9447f/ignoreit-settings.webp 1294w&quot;
              sizes=&quot;(max-width: 820px) 100vw, 820px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/55e2e78887e405bfb982ec62e9cb61d3/85860/ignoreit-settings.png 205w,
/static/55e2e78887e405bfb982ec62e9cb61d3/d68e4/ignoreit-settings.png 410w,
/static/55e2e78887e405bfb982ec62e9cb61d3/9f82e/ignoreit-settings.png 820w,
/static/55e2e78887e405bfb982ec62e9cb61d3/0d0e4/ignoreit-settings.png 1230w,
/static/55e2e78887e405bfb982ec62e9cb61d3/d4377/ignoreit-settings.png 1294w&quot;
            sizes=&quot;(max-width: 820px) 100vw, 820px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/55e2e78887e405bfb982ec62e9cb61d3/9f82e/ignoreit-settings.png&quot;
            alt=&quot;IGNOREIT settings in VS Code&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;em&gt;The two settings: the list of files and folders, and the .env.example option&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Add Item&lt;/strong&gt; to add an entry. Or open &lt;code class=&quot;language-text&quot;&gt;settings.json&lt;/code&gt; (&lt;strong&gt;Command Palette → Preferences: Open
User Settings (JSON)&lt;/strong&gt;) and edit the list directly:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ignoreit.array&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.env&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.pem&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.key&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;node_modules&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;coverage&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The default is &lt;code class=&quot;language-text&quot;&gt;[&quot;.env&quot;, &quot;*.pem&quot;]&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Put it in your &lt;strong&gt;User&lt;/strong&gt; settings to protect every project you open.&lt;/li&gt;
&lt;li&gt;Put it in a project&apos;s &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;.vscode/settings.json&lt;/code&gt;&lt;/strong&gt; to use a different list for that project.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After changing the list, run &lt;strong&gt;Developer: Reload Window&lt;/strong&gt; so the extension picks it up.&lt;/p&gt;
&lt;h3 id=&quot;starter-lists&quot;&gt;Starter lists&lt;/h3&gt;
&lt;p&gt;A few lists worth copying. Add only what you never want committed:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Entries&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Any project&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;.env.local&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;*.pem&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;*.key&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;*.p12&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;node_modules&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;coverage&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;.next&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;dist&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;.venv&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;venv&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;__pycache__&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;.pytest_cache&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terraform&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;.terraform&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;*.tfstate&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;*.tfstate.backup&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editors and OS&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;.DS_Store&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;.idea&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Folders are written &lt;strong&gt;without&lt;/strong&gt; a trailing slash: &lt;code class=&quot;language-text&quot;&gt;node_modules&lt;/code&gt;, not &lt;code class=&quot;language-text&quot;&gt;node_modules/&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;when-it-runs&quot;&gt;When it runs&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Automatically&lt;/strong&gt;, whenever files in the workspace change, for example when you save. Changes to
&lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;.env.example&lt;/code&gt; themselves don&apos;t trigger it, so it never loops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On demand&lt;/strong&gt;, with the command &lt;strong&gt;ignoreit: auto ignore files and folders&lt;/strong&gt; from the Command
Palette, or the shortcut &lt;strong&gt;Cmd + Shift + I&lt;/strong&gt; (macOS) / &lt;strong&gt;Ctrl + Shift + I&lt;/strong&gt; (Windows, Linux).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;a-walk-through&quot;&gt;A walk-through&lt;/h2&gt;
&lt;p&gt;Here&apos;s a new project with a &lt;code class=&quot;language-text&quot;&gt;.git&lt;/code&gt; folder and no &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; yet:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;my-api/
├── .git/
├── .env
├── server.pem
├── node_modules/
├── package.json
└── src/&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With this list:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;ignoreit.array&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.env&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.pem&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;node_modules&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;coverage&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;IGNOREIT shows &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;.env, *.pem, node_modules added to .gitignore&lt;/code&gt;&lt;/strong&gt; and creates:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;.env
*.pem
node_modules&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;coverage&lt;/code&gt; isn&apos;t added yet, because there&apos;s no &lt;code class=&quot;language-text&quot;&gt;coverage&lt;/code&gt; folder. Run your tests, and as soon as
&lt;code class=&quot;language-text&quot;&gt;coverage/&lt;/code&gt; appears you&apos;ll see &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;coverage added to .gitignore&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;.env
*.pem
node_modules
coverage&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Run it again with nothing new, and nothing happens. It never adds the same entry twice.&lt;/p&gt;
&lt;h3 id=&quot;with-an-existing-gitignore&quot;&gt;With an existing .gitignore&lt;/h3&gt;
&lt;p&gt;Your file is kept as it is, and only what&apos;s missing is appended. If the project already has:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# mine
dist/
.env*&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;and your list is &lt;code class=&quot;language-text&quot;&gt;[&quot;.env&quot;, &quot;*.pem&quot;]&lt;/code&gt; with a &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; and a &lt;code class=&quot;language-text&quot;&gt;server.pem&lt;/code&gt; in the project, only &lt;code class=&quot;language-text&quot;&gt;*.pem&lt;/code&gt;
is added. &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; is skipped because &lt;code class=&quot;language-text&quot;&gt;.env*&lt;/code&gt; already covers it.&lt;/p&gt;
&lt;h2 id=&quot;keep-envexample-in-sync&quot;&gt;Keep .env.example in sync&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;.env.example&lt;/code&gt; is the file you &lt;strong&gt;do&lt;/strong&gt; commit. It lists the variables the app needs, without their
values, so a new teammate can copy it to &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; and fill it in. Keeping it up to date by hand is
easy to forget. Turn on the second setting:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ignoreit.array&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.env&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.pem&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ignoreit.updateEnvExample&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It only runs when &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; is in &lt;code class=&quot;language-text&quot;&gt;ignoreit.array&lt;/code&gt; and the project has a &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When there&apos;s no &lt;code class=&quot;language-text&quot;&gt;.env.example&lt;/code&gt;,&lt;/strong&gt; it creates one from &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; with every value removed. Comments
and blank lines are kept:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .env&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Database&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;DATABASE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;postgres://admin:s3cret@localhost:5432/app
&lt;span class=&quot;token assign-left variable&quot;&gt;REDIS_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;redis://localhost:6379

&lt;span class=&quot;token assign-left variable&quot;&gt;STRIPE_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;sk_live_51H…&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .env.example (generated)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Database&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;DATABASE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;REDIS_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;

&lt;span class=&quot;token assign-left variable&quot;&gt;STRIPE_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;When &lt;code class=&quot;language-text&quot;&gt;.env.example&lt;/code&gt; already exists,&lt;/strong&gt; it only &lt;strong&gt;appends&lt;/strong&gt; variables that are new. Your
comments, ordering and example values are kept:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .env.example, before&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# documented&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;example-a&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Add &lt;code class=&quot;language-text&quot;&gt;B&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;C&lt;/code&gt; to &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt;, save, and it becomes:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .env.example, after&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# documented&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;example-a
&lt;span class=&quot;token assign-left variable&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It never removes a variable from &lt;code class=&quot;language-text&quot;&gt;.env.example&lt;/code&gt;, so if you delete one from &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt;, remove it from
the example yourself.&lt;/p&gt;
&lt;h2 id=&quot;good-to-know&quot;&gt;Good to know&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It looks at the top level of the project.&lt;/strong&gt; A &lt;code class=&quot;language-text&quot;&gt;server.pem&lt;/code&gt; in the project root is detected;
one inside &lt;code class=&quot;language-text&quot;&gt;certs/&lt;/code&gt; isn&apos;t. The &lt;code class=&quot;language-text&quot;&gt;*.pem&lt;/code&gt; line it writes still ignores &lt;code class=&quot;language-text&quot;&gt;.pem&lt;/code&gt; files at any depth,
because that&apos;s how &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; patterns work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write folders without a trailing slash&lt;/strong&gt; in the list (&lt;code class=&quot;language-text&quot;&gt;node_modules&lt;/code&gt;). If your &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;
already says &lt;code class=&quot;language-text&quot;&gt;node_modules/&lt;/code&gt;, IGNOREIT may add a plain &lt;code class=&quot;language-text&quot;&gt;node_modules&lt;/code&gt; line too. Both mean the
same thing to git, so it&apos;s harmless.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be careful with &lt;code class=&quot;language-text&quot;&gt;.env*&lt;/code&gt;.&lt;/strong&gt; It catches &lt;code class=&quot;language-text&quot;&gt;.env.local&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;.env.production&lt;/code&gt;, but in &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;
it also hides &lt;code class=&quot;language-text&quot;&gt;.env.example&lt;/code&gt;, which you want to commit. List the exact files instead (&lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt;,
&lt;code class=&quot;language-text&quot;&gt;.env.local&lt;/code&gt;), or add &lt;code class=&quot;language-text&quot;&gt;!.env.example&lt;/code&gt; to your &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; after the &lt;code class=&quot;language-text&quot;&gt;.env*&lt;/code&gt; line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-root workspaces:&lt;/strong&gt; it works on the first folder of the workspace.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It doesn&apos;t scan file contents.&lt;/strong&gt; It matches names. Hard-coded secrets in source files need a
secret scanner, such as GitHub&apos;s push protection.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;already-committed-a-secret&quot;&gt;Already committed a secret?&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; only stops &lt;strong&gt;untracked&lt;/strong&gt; files from being added. If a file is already in git, ignoring
it does nothing. To fix it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Rotate the secret first.&lt;/strong&gt; Assume it&apos;s compromised: change the password, revoke the key.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Stop tracking the file, keeping it on disk:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--cached&lt;/span&gt; .env
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Stop tracking .env&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If the repository is shared or public, the file is still in history. Remove it with a tool like
&lt;a href=&quot;https://github.com/newren/git-filter-repo&quot;&gt;git filter-repo&lt;/a&gt;, then force-push and ask everyone
to re-clone. This is why rotating comes first.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;settings&quot;&gt;Settings&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ignoreit.array&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;[&quot;.env&quot;, &quot;*.pem&quot;]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Files, folders and wildcard patterns to always ignore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;ignoreit.updateEnvExample&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep &lt;code class=&quot;language-text&quot;&gt;.env.example&lt;/code&gt; in sync with the variable names in &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Shortcut&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ignoreit: auto ignore files and folders&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cmd + Shift + I&lt;/strong&gt; (macOS), &lt;strong&gt;Ctrl + Shift + I&lt;/strong&gt; (Windows, Linux)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;version-history&quot;&gt;Version history&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;March 2020&lt;/td&gt;
&lt;td&gt;First release on the VS Code Marketplace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;January 2023&lt;/td&gt;
&lt;td&gt;Wildcard patterns such as &lt;code class=&quot;language-text&quot;&gt;*.pem&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;September 2025&lt;/td&gt;
&lt;td&gt;Full glob matching, and existing &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; patterns respected (7.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;December 2025&lt;/td&gt;
&lt;td&gt;Existing &lt;code class=&quot;language-text&quot;&gt;.env.example&lt;/code&gt; files are kept; only new variables are appended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;August 2026&lt;/td&gt;
&lt;td&gt;7.6.2: security update to a development dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;links&quot;&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=marcdomain.ignoreit&quot;&gt;VS Code Marketplace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/marcdomain/ignoreit&quot;&gt;Source on GitHub&lt;/a&gt;: issues and pull requests welcome&lt;/li&gt;
&lt;li&gt;If it saves you from a leaked key, you can &lt;a href=&quot;https://www.buymeacoffee.com/marcdomain&quot;&gt;buy me a coffee&lt;/a&gt; ☕️&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[How to generate Cucumber test reports]]></title><description><![CDATA[Three ways to get readable Cucumber reports in Node.js: the built-in HTML formatter, cucumber-html-reporter from JSON, and publishing reports from CI.]]></description><link>https://marcdomain.dev/cucumber-reports/</link><guid isPermaLink="false">https://marcdomain.dev/cucumber-reports/</guid><pubDate>Sat, 22 Feb 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This follows on from
&lt;a href=&quot;/selenium-cucumber/&quot;&gt;end-to-end test automation with Selenium and Cucumber&lt;/a&gt;.
Terminal output is fine while you write tests, but when a run fails in CI, or you want to show
results to someone else, you need a report: every feature, scenario and step, with timings,
errors and screenshots.&lt;/p&gt;
&lt;p&gt;Cucumber.js produces reports through &lt;strong&gt;formatters&lt;/strong&gt;. You can use several at once, each writing to
its own file.&lt;/p&gt;
&lt;h2 id=&quot;option-1-the-built-in-html-report&quot;&gt;Option 1: the built-in HTML report&lt;/h2&gt;
&lt;p&gt;Modern Cucumber (&lt;code class=&quot;language-text&quot;&gt;@cucumber/cucumber&lt;/code&gt;) has an HTML formatter built in. Nothing to install:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx cucumber-js &lt;span class=&quot;token parameter variable&quot;&gt;--format&lt;/span&gt; html:reports/cucumber-report.html&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or set it once in &lt;code class=&quot;language-text&quot;&gt;cucumber.js&lt;/code&gt;, alongside the console output:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// cucumber.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;features/**/*.feature&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;features/**/*.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;progress-bar&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;html:reports/cucumber-report.html&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The result is a single, self-contained HTML file you can open anywhere. It has a search box,
filters by status, and shows attachments, such as the screenshots our &lt;code class=&quot;language-text&quot;&gt;After&lt;/code&gt; hook adds when a
step fails:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// features/support/hooks.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; After&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Status &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;@cucumber/cucumber&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;After&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; result &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; Status&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FAILED&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; png &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;takeScreenshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;attach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;png&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;base64&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;image/png&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For most projects, this is all you need.&lt;/p&gt;
&lt;h2 id=&quot;option-2-a-bootstrap-style-report-from-json&quot;&gt;Option 2: a Bootstrap-style report from JSON&lt;/h2&gt;
&lt;p&gt;If you prefer a dashboard with charts and custom metadata, write a JSON report and convert it with
&lt;a href=&quot;https://www.npmjs.com/package/cucumber-html-reporter&quot;&gt;cucumber-html-reporter&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; --save-dev cucumber-html-reporter&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Add a JSON formatter:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// cucumber.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;features/**/*.feature&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;features/**/*.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;progress-bar&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;json:reports/cucumber-report.json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then a small script that turns the JSON into HTML:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// scripts/report.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; reporter &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;cucumber-html-reporter&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

reporter&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;bootstrap&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;jsonFile&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;reports/cucumber-report.json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;reports/cucumber-report.html&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;reportSuiteAsScenarios&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;scenarioTimestamp&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;launchReport&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CI&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// open it in a browser locally, not in CI&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;App version&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;APP_VERSION&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;local&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;Browser&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Chrome&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;Platform&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;platform&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;Executed&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CI&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;CI&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Local&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;test:e2e&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;cucumber-js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;test:e2e:report&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;node scripts/report.js&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Run the tests, then build the report. Use &lt;code class=&quot;language-text&quot;&gt;;&lt;/code&gt; (or a &lt;code class=&quot;language-text&quot;&gt;posttest&lt;/code&gt; script) so the report is generated
even when tests fail:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run test:e2e&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run test:e2e:report&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Screenshots attached in hooks are embedded in the JSON, so they show up here too.&lt;/p&gt;
&lt;h2 id=&quot;option-3-publish-reports-from-ci&quot;&gt;Option 3: publish reports from CI&lt;/h2&gt;
&lt;p&gt;Keep the report from every CI run as a downloadable artifact:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .github/workflows/e2e.yml (steps)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm run test&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;e2e&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;headless
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm run test&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;e2e&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;report
  &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; always()
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/upload&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;artifact@v4
  &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; always()
  &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cucumber&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;report
    &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; reports/&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;if: always()&lt;/code&gt; makes both steps run after a failure, which is exactly when you want the report.
Download it from the run&apos;s &lt;strong&gt;Summary&lt;/strong&gt; page under &lt;strong&gt;Artifacts&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;keep-reports-out-of-git&quot;&gt;Keep reports out of git&lt;/h2&gt;
&lt;p&gt;Reports are build output. Add the folder to &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;reports/&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;which-one-to-pick&quot;&gt;Which one to pick&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Built-in HTML&lt;/th&gt;
&lt;th&gt;cucumber-html-reporter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Install&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;td&gt;One dev dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steps&lt;/td&gt;
&lt;td&gt;One formatter&lt;/td&gt;
&lt;td&gt;JSON formatter + a script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshots&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charts and custom metadata&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Start with the built-in HTML formatter. Add the JSON route when you need charts or extra metadata
for stakeholders.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[End-to-end test automation with Selenium and Cucumber]]></title><description><![CDATA[Set up end-to-end tests in Node.js with selenium-webdriver and Cucumber: readable Gherkin scenarios, a shared browser per scenario, screenshots on failure, and a CI workflow.]]></description><link>https://marcdomain.dev/selenium-cucumber/</link><guid isPermaLink="false">https://marcdomain.dev/selenium-cucumber/</guid><pubDate>Sat, 22 Feb 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;End-to-end (E2E) tests drive your application the way a user would: open a browser, click, type,
and check what appears. They catch the bugs unit tests can&apos;t, like a broken form, a wrong redirect
or a missing button.&lt;/p&gt;
&lt;p&gt;In this guide we&apos;ll use two tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.selenium.dev/documentation/webdriver/&quot;&gt;selenium-webdriver&lt;/a&gt;&lt;/strong&gt; to control a real
browser (Chrome here, but Firefox, Edge and Safari work too).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/cucumber/cucumber-js&quot;&gt;Cucumber&lt;/a&gt;&lt;/strong&gt; to write the scenarios in plain English
(Gherkin), so product owners and testers can read them too.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the end you&apos;ll have a project that runs locally with a visible browser, runs headless in CI, and
saves a screenshot of every failing step.&lt;/p&gt;
&lt;h2 id=&quot;what-you-need&quot;&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Node.js 22 or newer&lt;/li&gt;
&lt;li&gt;Google Chrome&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don&apos;t need to install ChromeDriver yourself. Since version 4.6, Selenium ships with
&lt;strong&gt;Selenium Manager&lt;/strong&gt;, which finds your browser and downloads the matching driver on first run.&lt;/p&gt;
&lt;h2 id=&quot;set-up-the-project&quot;&gt;Set up the project&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; e2e &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; e2e
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; init &lt;span class=&quot;token parameter variable&quot;&gt;-y&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; --save-dev @cucumber/cucumber selenium-webdriver&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;The Cucumber package used to be called &lt;code class=&quot;language-text&quot;&gt;cucumber&lt;/code&gt;. It&apos;s now &lt;code class=&quot;language-text&quot;&gt;@cucumber/cucumber&lt;/code&gt;; if you&apos;re
updating an old project, swap the package and the &lt;code class=&quot;language-text&quot;&gt;require&lt;/code&gt; lines.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Create this layout:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;e2e/
├── cucumber.js
├── package.json
└── features/
    ├── web-form.feature
    ├── step_definitions/
    │   └── web-form.steps.js
    └── support/
        ├── world.js
        └── hooks.js&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;configure-cucumber&quot;&gt;Configure Cucumber&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;cucumber.js&lt;/code&gt; holds the default options, so &lt;code class=&quot;language-text&quot;&gt;npx cucumber-js&lt;/code&gt; needs no flags:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// cucumber.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;features/**/*.feature&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;features/**/*.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;progress-bar&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;html:reports/cucumber-report.html&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Add scripts to &lt;code class=&quot;language-text&quot;&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;test:e2e&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;cucumber-js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;test:e2e:headless&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;HEADLESS=1 cucumber-js&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;write-the-scenarios&quot;&gt;Write the scenarios&lt;/h2&gt;
&lt;p&gt;We&apos;ll test Selenium&apos;s own &lt;a href=&quot;https://www.selenium.dev/selenium/web/web-form.html&quot;&gt;demo form&lt;/a&gt;, a
stable page made for exactly this. Testing against a real search engine tends to break: they show
consent pages and block automated browsers.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;gherkin&quot;&gt;&lt;pre class=&quot;language-gherkin&quot;&gt;&lt;code class=&quot;language-gherkin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# features/web-form.feature&lt;/span&gt;
&lt;span class=&quot;token feature&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;Feature:&lt;/span&gt;&lt;span class=&quot;token important&quot;&gt; Web form&lt;/span&gt;
  As a visitor
  I want to submit the web form
  So that my input is received
&lt;/span&gt;
  &lt;span class=&quot;token scenario&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;Background:&lt;/span&gt;&lt;span class=&quot;token important&quot;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token atrule&quot;&gt;Given&lt;/span&gt; &lt;span class=&quot;token atrule&quot;&gt;I&lt;/span&gt; open the web form

  &lt;span class=&quot;token scenario&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;Scenario:&lt;/span&gt;&lt;span class=&quot;token important&quot;&gt; Submitting the form shows a confirmation&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token atrule&quot;&gt;When&lt;/span&gt; &lt;span class=&quot;token atrule&quot;&gt;I&lt;/span&gt; type &lt;span class=&quot;token string&quot;&gt;&quot;Hello Selenium&quot;&lt;/span&gt; into the text input
    &lt;span class=&quot;token atrule&quot;&gt;And&lt;/span&gt; &lt;span class=&quot;token atrule&quot;&gt;I&lt;/span&gt; submit the form
    &lt;span class=&quot;token atrule&quot;&gt;Then&lt;/span&gt; &lt;span class=&quot;token atrule&quot;&gt;I&lt;/span&gt; should see the message &lt;span class=&quot;token string&quot;&gt;&quot;Received!&quot;&lt;/span&gt;

  &lt;span class=&quot;token scenario&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;Scenario Outline:&lt;/span&gt;&lt;span class=&quot;token important&quot;&gt; The form accepts different inputs&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token atrule&quot;&gt;When&lt;/span&gt; &lt;span class=&quot;token atrule&quot;&gt;I&lt;/span&gt; type &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token outline variable&quot;&gt;&amp;lt;text&gt;&lt;/span&gt;&quot;&lt;/span&gt; into the text input
    &lt;span class=&quot;token atrule&quot;&gt;And&lt;/span&gt; &lt;span class=&quot;token atrule&quot;&gt;I&lt;/span&gt; submit the form
    &lt;span class=&quot;token atrule&quot;&gt;Then&lt;/span&gt; &lt;span class=&quot;token atrule&quot;&gt;I&lt;/span&gt; should see the message &lt;span class=&quot;token string&quot;&gt;&quot;Received!&quot;&lt;/span&gt;

    &lt;span class=&quot;token scenario&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;Examples:&lt;/span&gt;&lt;span class=&quot;token important&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token table-head&quot;&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token th variable&quot;&gt; text          &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token table-body&quot;&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token td string&quot;&gt; short         &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token td string&quot;&gt; with spaces   &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token td string&quot;&gt; 12345         &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Background&lt;/code&gt; runs before every scenario, and a &lt;code class=&quot;language-text&quot;&gt;Scenario Outline&lt;/code&gt; runs once per row of
&lt;code class=&quot;language-text&quot;&gt;Examples&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;create-a-browser-for-each-scenario&quot;&gt;Create a browser for each scenario&lt;/h2&gt;
&lt;p&gt;Cucumber gives every scenario a fresh &lt;strong&gt;World&lt;/strong&gt; object (&lt;code class=&quot;language-text&quot;&gt;this&lt;/code&gt; inside steps and hooks). We&apos;ll
put the browser on it, so each scenario starts clean.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// features/support/world.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; setWorldConstructor&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setDefaultTimeout&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; World &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;@cucumber/cucumber&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Builder&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Browser &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;selenium-webdriver&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; chrome &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;selenium-webdriver/chrome&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;setDefaultTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BrowserWorld&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;World&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;openBrowser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; options &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;chrome&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Options&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addArguments&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;--window-size=1366,900&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;HEADLESS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addArguments&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;--headless=new&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Builder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forBrowser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Browser&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CHROME&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setChromeOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;setWorldConstructor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;BrowserWorld&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The hooks open the browser before each scenario, and close it afterwards. If a scenario fails,
they attach a screenshot to the report first.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// features/support/hooks.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Before&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; After&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Status &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;@cucumber/cucumber&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;Before&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;openBrowser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;After&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; result &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; Status&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FAILED&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; png &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;takeScreenshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;attach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;png&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;base64&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;image/png&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;write-the-steps&quot;&gt;Write the steps&lt;/h2&gt;
&lt;p&gt;Each Gherkin line maps to a step definition. &lt;code class=&quot;language-text&quot;&gt;{string}&lt;/code&gt; captures a quoted value from the scenario.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// features/step_definitions/web-form.steps.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Given&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; When&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Then &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;@cucumber/cucumber&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; By&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; until &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;selenium-webdriver&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; assert &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;node:assert/strict&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;FORM_URL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://www.selenium.dev/selenium/web/web-form.html&quot;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;Given&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;I open the web form&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FORM_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  assert&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;equal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getTitle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Web form&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;When&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;I type {string} into the text input&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; input &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;By&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;my-text-id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; input&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; input&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sendKeys&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;When&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;I submit the form&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;By&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;button[type=&apos;submit&apos;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;Then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;I should see the message {string}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; message &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;driver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;until&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;elementLocated&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;By&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  assert&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;equal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; expected&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A few habits that keep E2E tests reliable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wait for things, don&apos;t sleep.&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;driver.wait(until.elementLocated(...))&lt;/code&gt; returns as soon as the
element exists. A fixed &lt;code class=&quot;language-text&quot;&gt;sleep&lt;/code&gt; is either too short (flaky) or too long (slow).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer stable locators.&lt;/strong&gt; IDs and &lt;code class=&quot;language-text&quot;&gt;data-testid&lt;/code&gt; attributes survive redesigns; long CSS paths
and XPath tied to layout don&apos;t.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One browser per scenario.&lt;/strong&gt; Shared browsers leak cookies and state between tests.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;run-the-tests&quot;&gt;Run the tests&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run test:e2e            &lt;span class=&quot;token comment&quot;&gt;# opens Chrome so you can watch&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run test:e2e:headless   &lt;span class=&quot;token comment&quot;&gt;# no window, as in CI&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;4 scenarios (4 passed)
24 steps (24 passed)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Open &lt;code class=&quot;language-text&quot;&gt;reports/cucumber-report.html&lt;/code&gt; in a browser to see every scenario and step, with screenshots
attached to any failures. To learn more about reports, read
&lt;a href=&quot;/cucumber-reports/&quot;&gt;how to generate Cucumber test reports&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;run-them-in-ci&quot;&gt;Run them in CI&lt;/h2&gt;
&lt;p&gt;GitHub&apos;s &lt;code class=&quot;language-text&quot;&gt;ubuntu-latest&lt;/code&gt; runners come with Chrome installed, and Selenium Manager fetches the
driver, so the workflow is short:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .github/workflows/e2e.yml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; e2e
&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;push&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pull_request&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;e2e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;node@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;node-version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;22&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm ci
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm run test&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;e2e&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;headless
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/upload&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;artifact@v4
        &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; always()
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cucumber&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;report
          &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; reports/&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;if: always()&lt;/code&gt; uploads the report even when tests fail, which is when you need it most.&lt;/p&gt;
&lt;h2 id=&quot;growing-the-suite&quot;&gt;Growing the suite&lt;/h2&gt;
&lt;p&gt;For a larger application, move locators and actions into &lt;strong&gt;page objects&lt;/strong&gt; (one class per page),
so steps read like &lt;code class=&quot;language-text&quot;&gt;await formPage.submit(&quot;Hello&quot;)&lt;/code&gt; and a changed selector is fixed in one place.
I wrote about structuring bigger suites in two parts:
&lt;a href=&quot;https://www.codementor.io/@marcuschukwuoma/how-to-use-cucumber-and-selenium-webdriver-effectively-for-bdd-test-automation-part-1-16yced7psb&quot;&gt;part one&lt;/a&gt;
and
&lt;a href=&quot;https://www.codementor.io/@marcuschukwuoma/how-to-use-cucumber-and-selenium-webdriver-effectively-for-bdd-test-automation-part-2-16yzushf80&quot;&gt;part two&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Configuration and change management with Packer and Ansible]]></title><description><![CDATA[Build an AWS machine image with Packer (HCL2), configure it with an Ansible playbook, and launch an EC2 instance that serves an Express app behind NGINX.]]></description><link>https://marcdomain.dev/packer-ansible/</link><guid isPermaLink="false">https://marcdomain.dev/packer-ansible/</guid><pubDate>Sat, 15 Feb 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this guide we&apos;ll automate the setup of a server end to end. &lt;strong&gt;Packer&lt;/strong&gt; builds an Amazon Machine
Image (AMI), &lt;strong&gt;Ansible&lt;/strong&gt; configures it (Node.js, an Express app, NGINX as a reverse proxy), and we
launch an EC2 instance from the result. Every server you launch from that image is identical, and
changing the setup means changing code, not clicking around a console.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 820px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAAC3ElEQVR42mNgAIL///8zgjADGqivr2dCE2KEYhTw/z+qGJiTVn+Gq6Z7elhhf3prQV9BfnZFmxqaoYz19QxgtlVAmqVbVHiJd2JImaGnnwnYUGSL8muaNLu6mq809E76n9EZ+ba4N/FP8YT038UTCgpB8qtWhTKDFcqEcialxS/NTfD6H5nm/Sut3Pl7Xl3i/8TijBlAWVaIaRaFnLPnz7syb1bvg+jcVq2dK5bYze+v6y5vLd4xYUXX/8nLpreC1Qn78bb3dO/s7qj9VlMcP21Wd2nLvGl9Ti6RkYmTFnX9z6jNaQOr65m+IHLdxo3/2/pnmoP4i/vzIxf1ZfWvmFRdVlhevmz++rn/izrbJ0cGeexqrMz8mZWX1bhqZk3lgp6MqUsm5/mA9ASm+HX6Jnp/Z2AIFWLo7O3unNDX/AYksXvVBDmQ4vWz68tWzWtN3za3ojq3KH93xbTi/z6JXh9zM5Mbts6vblgxuzV9/bzmkhUza4r/v18vwKPtYK3pFvRfwz3UkqG8sbMqq6jq08yZvbqrpxQXLp1cPmt2T54rMNaZS/Mi880iyr/pB/g/8C9zeucfm3pkYW9G1LVru4VntWf5L55cMmfvwurY+OLStLSutj8a9oE6DDGFTfo9DW1fJ06csH/Ngpr8Y9umB4Jc6xgarZ7fNv2BX17HHQYuLQlT75BoA8/o/5b+cQugSY31yIqu4BltOfWzV6x42bNg6Rl4kuromVw9Y/KM/0WZGU8DoxMbHFKaujNaZn8r7Zx+zcTWTRaWGgxcQiOT6pr++xYUnLdJSckNLUhrK2it+1TW3ffN2D3YgQGWqEF0XlpeVEls4imX8KzvtinNb9ySKqaI61mKQZM4Eyw9qtn72NglJR92y8n87ZOT880lIW2bhIGdKdQwRtSUHhrKLGofysOgpcUGzQOMEIyaCaA0DxBzw8XQcxrIBSipHeIijGwGci1yNoWwEVkUALk4Pn2OwCX/AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/ab165a339888a9dcc442756668cbd72f/ac387/change-management.webp 205w,
/static/ab165a339888a9dcc442756668cbd72f/0cb6c/change-management.webp 410w,
/static/ab165a339888a9dcc442756668cbd72f/b1f58/change-management.webp 820w,
/static/ab165a339888a9dcc442756668cbd72f/fc6cc/change-management.webp 1230w,
/static/ab165a339888a9dcc442756668cbd72f/af3f0/change-management.webp 1280w&quot;
              sizes=&quot;(max-width: 820px) 100vw, 820px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/ab165a339888a9dcc442756668cbd72f/85860/change-management.png 205w,
/static/ab165a339888a9dcc442756668cbd72f/d68e4/change-management.png 410w,
/static/ab165a339888a9dcc442756668cbd72f/9f82e/change-management.png 820w,
/static/ab165a339888a9dcc442756668cbd72f/0d0e4/change-management.png 1230w,
/static/ab165a339888a9dcc442756668cbd72f/21b4d/change-management.png 1280w&quot;
            sizes=&quot;(max-width: 820px) 100vw, 820px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/ab165a339888a9dcc442756668cbd72f/9f82e/change-management.png&quot;
            alt=&quot;change-management&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;em&gt;Configuration and change management&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;configuration-vs-change-management&quot;&gt;Configuration vs. change management&lt;/h2&gt;
&lt;p&gt;They&apos;re related, but not the same:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Configuration management&lt;/strong&gt; keeps a known, versioned record of how your systems are set up:
packages and their versions, config files, users, network addresses. If a server dies, you can
rebuild it exactly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change management&lt;/strong&gt; is how changes to that setup are proposed, reviewed, applied and rolled
back.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keeping both in code (here, a Packer template and an Ansible playbook in git) gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reliability.&lt;/strong&gt; The setup is written down, not in someone&apos;s head. Failures are easier to
recover from because rebuilding is a command.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A history.&lt;/strong&gt; Git shows what changed, when and why, which makes the next decision easier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lower cost and risk.&lt;/strong&gt; Fewer one-off servers, less drift, and reviewable changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-tools&quot;&gt;The tools&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.hashicorp.com/packer&quot;&gt;Packer&lt;/a&gt;&lt;/strong&gt; builds machine images from a template. It
starts a temporary instance, runs your provisioners on it, saves the disk as an image, and cleans
up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://docs.ansible.com/&quot;&gt;Ansible&lt;/a&gt;&lt;/strong&gt; describes the desired state of a machine in YAML
playbooks and applies it over SSH. Its modules are idempotent: running a playbook twice gives the
same result.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Baking the configuration into an image (instead of configuring servers after they boot) is often
called &lt;strong&gt;immutable infrastructure&lt;/strong&gt;: to change a server, you build a new image and replace it.&lt;/p&gt;
&lt;h2 id=&quot;what-you-need&quot;&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;An AWS account and credentials that can create EC2 instances, AMIs, key pairs and security
groups&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.hashicorp.com/packer/install&quot;&gt;Packer&lt;/a&gt; 1.10 or newer&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ansible.com/ansible/latest/installation_guide/&quot;&gt;Ansible&lt;/a&gt; on your machine (Packer&apos;s
&lt;code class=&quot;language-text&quot;&gt;ansible&lt;/code&gt; provisioner runs it from your machine over SSH)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On macOS:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew tap hashicorp/tap
brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; hashicorp/tap/packer
brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ansible&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;On Windows, Packer is available with &lt;code class=&quot;language-text&quot;&gt;choco install packer&lt;/code&gt;. Ansible&apos;s control node doesn&apos;t run on
Windows directly; use WSL.&lt;/p&gt;
&lt;h2 id=&quot;aws-credentials&quot;&gt;AWS credentials&lt;/h2&gt;
&lt;p&gt;Packer uses the standard AWS credential chain, so whatever works for the AWS CLI works here: an
SSO profile, &lt;code class=&quot;language-text&quot;&gt;~/.aws/credentials&lt;/code&gt;, or environment variables.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;aws configure sso          &lt;span class=&quot;token comment&quot;&gt;# or: aws configure&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;AWS_PROFILE&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;my-profile
&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;AWS_REGION&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;eu-west-1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you prefer a &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; file, keep it out of git. If you use VS Code,
&lt;a href=&quot;/ignoreit/&quot;&gt;IGNOREIT&lt;/a&gt; adds &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt; to &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; for you and can keep a
&lt;code class=&quot;language-text&quot;&gt;.env.example&lt;/code&gt; in sync.&lt;/p&gt;
&lt;h2 id=&quot;project-layout&quot;&gt;Project layout&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;packer-ansible/
├── packer/
│   └── express-app.pkr.hcl
└── ansible/
    ├── playbook.yml
    └── templates/
        ├── express-app.service.j2
        └── nginx-site.conf.j2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;the-packer-template&quot;&gt;The Packer template&lt;/h2&gt;
&lt;p&gt;Packer templates are written in HCL2 (the old JSON format is deprecated). Plugins are separate
from Packer, and the &lt;code class=&quot;language-text&quot;&gt;required_plugins&lt;/code&gt; block lets &lt;code class=&quot;language-text&quot;&gt;packer init&lt;/code&gt; install them.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;hcl&quot;&gt;&lt;pre class=&quot;language-hcl&quot;&gt;&lt;code class=&quot;language-hcl&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# packer/express-app.pkr.hcl&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;packer&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;required_plugins&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;amazon&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;source&lt;/span&gt;  &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;github.com/hashicorp/amazon&quot;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&gt;= 1.3.0&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;ansible&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;source&lt;/span&gt;  &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;github.com/hashicorp/ansible&quot;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&gt;= 1.1.0&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;variable&lt;span class=&quot;token type variable&quot;&gt; &quot;region&quot; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;type&lt;/span&gt;    &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; string
  &lt;span class=&quot;token property&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;eu-west-1&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;variable&lt;span class=&quot;token type variable&quot;&gt; &quot;instance_type&quot; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;type&lt;/span&gt;    &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; string
  &lt;span class=&quot;token property&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;t3.micro&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;locals&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; formatdate(&lt;span class=&quot;token string&quot;&gt;&quot;YYYYMMDD-hhmmss&quot;&lt;/span&gt;, timestamp())
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

source &lt;span class=&quot;token string&quot;&gt;&quot;amazon-ebs&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ubuntu&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;region&lt;/span&gt;        &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; var.region
  &lt;span class=&quot;token property&quot;&gt;instance_type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; var.instance_type
  &lt;span class=&quot;token property&quot;&gt;ssh_username&lt;/span&gt;  &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ubuntu&quot;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;ami_name&lt;/span&gt;      &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;express-app-&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token type variable&quot;&gt;timestamp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;# Latest Ubuntu 24.04 LTS image published by Canonical&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;source_ami_filter&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;filters&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;name&lt;/span&gt;                &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*&quot;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;root-device-type&lt;/span&gt;    &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ebs&quot;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;virtualization-type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;hvm&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;owners&lt;/span&gt;      &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;099720109477&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Canonical&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;most_recent&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token property&quot;&gt;tags&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;Name&lt;/span&gt;    &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;express-app&quot;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;BuiltBy&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;packer&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;build&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;sources&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;source.amazon-ebs.ubuntu&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;provisioner&lt;span class=&quot;token type variable&quot;&gt; &quot;ansible&quot; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;playbook_file&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../ansible/playbook.yml&quot;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;user&lt;/span&gt;          &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ubuntu&quot;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;use_proxy&lt;/span&gt;     &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A few things to note:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;AMI name includes a timestamp&lt;/strong&gt;, because AMI names must be unique. Every build creates a
new image instead of failing on the second run.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;owners = [&quot;099720109477&quot;]&lt;/code&gt;&lt;/strong&gt; is Canonical&apos;s account, so you only ever get official Ubuntu
images.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;use_proxy = false&lt;/code&gt;&lt;/strong&gt; lets Ansible connect straight to the build instance over SSH, which
avoids problems with the plugin&apos;s local proxy on newer OpenSSH versions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-ansible-playbook&quot;&gt;The Ansible playbook&lt;/h2&gt;
&lt;p&gt;The playbook installs Node.js 22 from NodeSource, deploys a
&lt;a href=&quot;https://github.com/marcdomain/basic-express-app&quot;&gt;small Express app&lt;/a&gt; (it listens on port 3000), runs
it as a systemd service, and puts NGINX in front on port 80.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# ansible/playbook.yml&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Configure the Express app server
  &lt;span class=&quot;token key atrule&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; all
  &lt;span class=&quot;token key atrule&quot;&gt;become&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;vars&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;app_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; express&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app
    &lt;span class=&quot;token key atrule&quot;&gt;app_user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu
    &lt;span class=&quot;token key atrule&quot;&gt;app_dir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /opt/express&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app
    &lt;span class=&quot;token key atrule&quot;&gt;app_repo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/marcdomain/basic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;express&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app.git
    &lt;span class=&quot;token key atrule&quot;&gt;app_port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3000&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;node_major&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;22&lt;/span&gt;

  &lt;span class=&quot;token key atrule&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Install base packages
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# acl lets Ansible run tasks as the app user; python3-debian is needed by deb822_repository&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;acl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ca&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;certificates&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; curl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; git&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; nginx&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; python3&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;debian&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; present
        &lt;span class=&quot;token key atrule&quot;&gt;update_cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Add the NodeSource repository and its signing key
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.deb822_repository&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nodesource
        &lt;span class=&quot;token key atrule&quot;&gt;types&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deb
        &lt;span class=&quot;token key atrule&quot;&gt;uris&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://deb.nodesource.com/node_{{ node_major }}.x&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;suites&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nodistro
        &lt;span class=&quot;token key atrule&quot;&gt;components&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; main
        &lt;span class=&quot;token key atrule&quot;&gt;signed_by&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//deb.nodesource.com/gpgkey/nodesource&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;repo.gpg.key

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Install Node.js
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nodejs
        &lt;span class=&quot;token key atrule&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; present
        &lt;span class=&quot;token key atrule&quot;&gt;update_cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Clone the app
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.git&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ app_repo }}&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ app_dir }}&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; master

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Give the app user the app folder
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ app_dir }}&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;owner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ app_user }}&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ app_user }}&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;recurse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Install production dependencies
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm install &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;omit=dev
      &lt;span class=&quot;token key atrule&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;chdir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ app_dir }}&quot;&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;become_user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ app_user }}&quot;&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;changed_when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Install the systemd service
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; templates/express&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;app.service.j2
        &lt;span class=&quot;token key atrule&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /etc/systemd/system/&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; app_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;.service
        &lt;span class=&quot;token key atrule&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0644&quot;&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;notify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Restart app

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Start the app on boot
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ app_name }}&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; started
        &lt;span class=&quot;token key atrule&quot;&gt;daemon_reload&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Remove the default NGINX site
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /etc/nginx/sites&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;enabled/default
        &lt;span class=&quot;token key atrule&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; absent
      &lt;span class=&quot;token key atrule&quot;&gt;notify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Reload nginx

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Add the reverse proxy site
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; templates/nginx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;site.conf.j2
        &lt;span class=&quot;token key atrule&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /etc/nginx/sites&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;available/&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; app_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0644&quot;&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;notify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Reload nginx

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Enable the reverse proxy site
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /etc/nginx/sites&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;available/&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; app_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /etc/nginx/sites&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;enabled/&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; app_name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; link
      &lt;span class=&quot;token key atrule&quot;&gt;notify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Reload nginx

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Start NGINX on boot
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
        &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; started

  &lt;span class=&quot;token key atrule&quot;&gt;handlers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Restart app
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ app_name }}&quot;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; restarted
        &lt;span class=&quot;token key atrule&quot;&gt;daemon_reload&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Reload nginx
      &lt;span class=&quot;token key atrule&quot;&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx
        &lt;span class=&quot;token key atrule&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; reloaded&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The systemd unit keeps the app running and restarts it if it crashes:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ini&quot;&gt;&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# ansible/templates/express-app.service.j2&lt;/span&gt;
&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;{{ app_name }}&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;After&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;network.target&lt;/span&gt;

&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Service&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;{{ app_user }}&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;WorkingDirectory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;{{ app_dir }}&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;NODE_ENV=production&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;ExecStart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;/usr/bin/node app.js&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;always&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;RestartSec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;3&lt;/span&gt;

&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;Install&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;WantedBy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;multi-user.target&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And the NGINX site forwards port 80 to the app:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;nginx&quot;&gt;&lt;pre class=&quot;language-nginx&quot;&gt;&lt;code class=&quot;language-nginx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# ansible/templates/nginx-site.conf.j2&lt;/span&gt;
&lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;server&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;listen&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt; default_server&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;server_name&lt;/span&gt; _&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;location&lt;/span&gt; /&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_pass&lt;/span&gt; http://127.0.0.1:&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; app_port &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_http_version&lt;/span&gt; 1.1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_set_header&lt;/span&gt; Host &lt;span class=&quot;token variable&quot;&gt;$host&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_set_header&lt;/span&gt; X-Real-IP &lt;span class=&quot;token variable&quot;&gt;$remote_addr&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_set_header&lt;/span&gt; X-Forwarded-For &lt;span class=&quot;token variable&quot;&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token directive&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;proxy_set_header&lt;/span&gt; X-Forwarded-Proto &lt;span class=&quot;token variable&quot;&gt;$scheme&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Why these choices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fully qualified module names&lt;/strong&gt; (&lt;code class=&quot;language-text&quot;&gt;ansible.builtin.apt&lt;/code&gt;) are the current Ansible style and avoid
clashes with collections.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Templates instead of &lt;code class=&quot;language-text&quot;&gt;echo&lt;/code&gt; in shell tasks&lt;/strong&gt; make config files reviewable, and Ansible only
changes them when their content changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handlers&lt;/strong&gt; restart or reload a service only when something it depends on actually changed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;systemd instead of a process manager&lt;/strong&gt; means one less tool to install: the OS starts the app on
boot and restarts it on failure.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;build-the-image&quot;&gt;Build the image&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; packer
packer init &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
packer &lt;span class=&quot;token function&quot;&gt;fmt&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
packer validate &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
packer build &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;init&lt;/code&gt; installs the Amazon and Ansible plugins.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;fmt&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;validate&lt;/code&gt; catch formatting and syntax errors before you spend money on an instance.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;build&lt;/code&gt; starts a temporary EC2 instance, runs the playbook on it, creates the AMI, and
terminates the instance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Override a variable at build time with &lt;code class=&quot;language-text&quot;&gt;-var&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;packer build &lt;span class=&quot;token parameter variable&quot;&gt;-var&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;region=us-east-1&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-var&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;instance_type=t3.small&quot;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;At the end, Packer prints the new AMI ID:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;==&gt; Builds finished. The artifacts of successful builds are:
--&gt; amazon-ebs.ubuntu: AMIs were created:
eu-west-1: ami-0123456789abcdef0&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;launch-an-instance-from-the-image&quot;&gt;Launch an instance from the image&lt;/h2&gt;
&lt;h3 id=&quot;with-the-aws-cli&quot;&gt;With the AWS CLI&lt;/h3&gt;
&lt;p&gt;Create a security group that allows HTTP from anywhere and SSH only from your IP:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;MY_IP&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; https://checkip.amazonaws.com&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token assign-left variable&quot;&gt;SG_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;aws ec2 create-security-group &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --group-name express-app &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--description&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;HTTP from anywhere, SSH from my IP&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--query&lt;/span&gt; GroupId &lt;span class=&quot;token parameter variable&quot;&gt;--output&lt;/span&gt; text&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;

aws ec2 authorize-security-group-ingress --group-id &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SG_ID&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--protocol&lt;/span&gt; tcp &lt;span class=&quot;token parameter variable&quot;&gt;--port&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--cidr&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;.0.0/0
aws ec2 authorize-security-group-ingress --group-id &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SG_ID&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--protocol&lt;/span&gt; tcp &lt;span class=&quot;token parameter variable&quot;&gt;--port&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;22&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--cidr&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$MY_IP&lt;/span&gt;/32&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then launch an instance from your AMI (replace the AMI ID and key pair name):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;INSTANCE_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;aws ec2 run-instances &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --image-id ami-0123456789abcdef0 &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --instance-type t3.micro &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --key-name my-key &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --security-group-ids &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SG_ID&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  --tag-specifications &lt;span class=&quot;token string&quot;&gt;&apos;ResourceType=instance,Tags=[{Key=Name,Value=express-app}]&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--query&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Instances[0].InstanceId&apos;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--output&lt;/span&gt; text&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;

aws ec2 &lt;span class=&quot;token function&quot;&gt;wait&lt;/span&gt; instance-running --instance-ids &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$INSTANCE_ID&lt;/span&gt;&quot;&lt;/span&gt;

aws ec2 describe-instances --instance-ids &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$INSTANCE_ID&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--query&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Reservations[0].Instances[0].PublicIpAddress&apos;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--output&lt;/span&gt; text&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Open &lt;code class=&quot;language-text&quot;&gt;http://&amp;lt;public-ip&gt;&lt;/code&gt; in your browser and you&apos;ll see &lt;strong&gt;It works! 🥂&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;with-the-console&quot;&gt;With the console&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Open the &lt;a href=&quot;https://console.aws.amazon.com/ec2/&quot;&gt;EC2 console&lt;/a&gt; in the region you built in.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Images → AMIs&lt;/strong&gt;, select your &lt;code class=&quot;language-text&quot;&gt;express-app-…&lt;/code&gt; image, and choose &lt;strong&gt;Launch instance from
AMI&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Pick an instance type (&lt;code class=&quot;language-text&quot;&gt;t3.micro&lt;/code&gt; is fine) and a key pair.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Network settings&lt;/strong&gt;, allow &lt;strong&gt;HTTP&lt;/strong&gt; from anywhere and &lt;strong&gt;SSH&lt;/strong&gt; from &lt;strong&gt;My IP&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Launch, then open the instance&apos;s &lt;strong&gt;Public IPv4 address&lt;/strong&gt; in your browser.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note that the security group opens port &lt;strong&gt;80&lt;/strong&gt; (NGINX), not 3000. The app only listens on
localhost; NGINX is the only way in.&lt;/p&gt;
&lt;h2 id=&quot;making-a-change&quot;&gt;Making a change&lt;/h2&gt;
&lt;p&gt;This is where the setup pays off. To change the server (a new Node.js version, an NGINX header, a
new app release):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Change the playbook or template, and open a pull request so the change is reviewed.&lt;/li&gt;
&lt;li&gt;Run &lt;code class=&quot;language-text&quot;&gt;packer build .&lt;/code&gt; to bake a new AMI.&lt;/li&gt;
&lt;li&gt;Launch new instances from it (or update your launch template / Auto Scaling group), check them,
then terminate the old ones.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Rolling back is launching from the previous AMI.&lt;/p&gt;
&lt;h2 id=&quot;clean-up&quot;&gt;Clean up&lt;/h2&gt;
&lt;p&gt;AMIs keep a snapshot, which costs money while it exists. When you&apos;re done:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;aws ec2 terminate-instances --instance-ids &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$INSTANCE_ID&lt;/span&gt;&quot;&lt;/span&gt;
aws ec2 deregister-image --image-id ami-0123456789abcdef0
&lt;span class=&quot;token comment&quot;&gt;# then delete the AMI&apos;s snapshot under EC2 → Snapshots&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The original code for this guide is on
&lt;a href=&quot;https://github.com/marcdomain/Configuration-and-Change-Management-with-Packer-and-Ansible&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[How to SSH into a CircleCI build container]]></title><description><![CDATA[Debug a failing CircleCI job from the inside: set up an SSH key, rerun the job with SSH, connect, reproduce the failure, and clean up safely.]]></description><link>https://marcdomain.dev/circleci-ssh/</link><guid isPermaLink="false">https://marcdomain.dev/circleci-ssh/</guid><pubDate>Thu, 23 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 820px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 51.21951219512195%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAIAAAA7N+mxAAAACXBIWXMAAAsTAAALEwEAmpwYAAABlElEQVR42mWS22rcMBCG/f4P0ovSi4ZCU0ppmgMtafdQiJfsJl7F9sqxJR9ky5ZlHUZbJUsgmw5iGGb+D0m/FLj9Hpw7LF+/Dt+xDjQYBdqAtU+CI0ngW5OxuOSTnx/jXmqcJaIpRO0z7AHewMa6XuqMjYPUysCooVfAFSjrlLVsGpOuvqsL1FKmpHFwBE/aRlmzSir0yBqhwmK6eBj+YIkauSqrCxr+yG+/JHfn+fqKhlxNzyd6gaUGUvOUctr0jdDrUp2jfobVX1J9TTbfi5tfefwzS6/J9iwPe30M+53XaTWP6D2uy17FTG9rHVWKjQb37Um8/Piw/JQuTpLFaXrDlXwy8oUPvE8JaeOC7WjXjuZg2cF4A1BJUcoBsY6OQy3FG0eDUdsdYRGuMGEefj2brMlEvRvqsKQ+Z6LxFvIJSmFLYaSBwIDz1z747Ovn54VZgS9xfJ0nn7P5BzR7t168R79Pszlq+Iqob1F/hgY6mGD/X/i/sWryZbnbMLrl9L4l245u2gJxKo1ho80689hpoeEfvCk1DnKBs3AAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/3084654e80b2ddd43521374ede6da64d/ac387/ssh-circleci-intro.webp 205w,
/static/3084654e80b2ddd43521374ede6da64d/0cb6c/ssh-circleci-intro.webp 410w,
/static/3084654e80b2ddd43521374ede6da64d/b1f58/ssh-circleci-intro.webp 820w,
/static/3084654e80b2ddd43521374ede6da64d/fc6cc/ssh-circleci-intro.webp 1230w,
/static/3084654e80b2ddd43521374ede6da64d/2463b/ssh-circleci-intro.webp 1640w,
/static/3084654e80b2ddd43521374ede6da64d/289e4/ssh-circleci-intro.webp 1688w&quot;
              sizes=&quot;(max-width: 820px) 100vw, 820px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/3084654e80b2ddd43521374ede6da64d/85860/ssh-circleci-intro.png 205w,
/static/3084654e80b2ddd43521374ede6da64d/d68e4/ssh-circleci-intro.png 410w,
/static/3084654e80b2ddd43521374ede6da64d/9f82e/ssh-circleci-intro.png 820w,
/static/3084654e80b2ddd43521374ede6da64d/0d0e4/ssh-circleci-intro.png 1230w,
/static/3084654e80b2ddd43521374ede6da64d/6ee58/ssh-circleci-intro.png 1640w,
/static/3084654e80b2ddd43521374ede6da64d/bf286/ssh-circleci-intro.png 1688w&quot;
            sizes=&quot;(max-width: 820px) 100vw, 820px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/3084654e80b2ddd43521374ede6da64d/9f82e/ssh-circleci-intro.png&quot;
            alt=&quot;circleci&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;em&gt;source: &lt;a href=&quot;https://circleci.com/docs/about-circleci/&quot;&gt;CircleCI docs&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When a CircleCI job runs, it gets a fresh container or VM (Linux, macOS, Windows…) that acts as
your machine for the length of the job. Your code is checked out, dependencies installed, tests
run, and artifacts built and deployed, one step after another. Each step&apos;s output feeds the next,
which is what makes it a &lt;strong&gt;pipeline&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When a step fails and the logs don&apos;t say why, the fastest fix is often to get inside that exact
environment and look around. CircleCI lets you do that by rerunning the job with SSH.&lt;/p&gt;
&lt;h2 id=&quot;why-ssh-into-a-build&quot;&gt;Why SSH into a build&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reproduce a failure&lt;/strong&gt; in the exact environment it happened in, and try fixes live.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check what&apos;s installed:&lt;/strong&gt; tool versions, packages, and whether a cache was restored.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Find files:&lt;/strong&gt; where something was written, or why a path doesn&apos;t exist.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inspect environment variables&lt;/strong&gt; without printing them in your build logs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check resources&lt;/strong&gt; when a job is slow or runs out of memory:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; /proc/meminfo    &lt;span class=&quot;token comment&quot;&gt;# memory (Linux)&lt;/span&gt;
nproc                &lt;span class=&quot;token comment&quot;&gt;# CPU cores&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;df&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-h&lt;/span&gt;                &lt;span class=&quot;token comment&quot;&gt;# disk&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A note on environment variables: you could add a step that runs &lt;code class=&quot;language-text&quot;&gt;printenv&lt;/code&gt;, but then every value
ends up in the build logs, visible to anyone who can see the project.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 606px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 44.390243902439025%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABdUlEQVR42pWRTU/CQBCGezcRC/RrCy393oKFQkuVigaMMfHqbzD+/5ull9eZJcQrHp7szuTdmXd2tKfj209R1t2qarrVZvt/qm1X1g9dsa66/ev7j1bEEb6kxPeHQP3pY3D0oN8MoRtj6KPrGI4N3N4N4AUhtCiKkQgXWSgwiQSMwIVpOTDt67EcgZFhIkxSaNNZSG4sGLYLR0xgmg4My/437HIWJdC8IFId4jRDtW3gTj0lsMk1501bKBec45jh+587Oil3Lhizw0DZ5USa53hsWyT0px45X5Zr1M0Dyk2FVOaQ8zmxQJJJlbtfrtA87tDsdvCDADytxh/JHcemhYzEL4cD2v2zEnIRLlpWNdYEF1+tN3DciYKnEJOpmsq07bNDQYE+HEEnhyzwZzMiIIcBLcdWYn4siKnvqwJD3uyF8XnLA10HT6tt2/0pXRSnbFH0sZz3USr7MMkI2cdZruLLeSEhXSIXfZqfkfQ2zYtT0z6ffgFFDvkDA8XtGAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/dc6b222732054f5bd81e1eb157928026/ac387/printenv.webp 205w,
/static/dc6b222732054f5bd81e1eb157928026/0cb6c/printenv.webp 410w,
/static/dc6b222732054f5bd81e1eb157928026/18e7e/printenv.webp 606w&quot;
              sizes=&quot;(max-width: 606px) 100vw, 606px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/dc6b222732054f5bd81e1eb157928026/85860/printenv.png 205w,
/static/dc6b222732054f5bd81e1eb157928026/d68e4/printenv.png 410w,
/static/dc6b222732054f5bd81e1eb157928026/4d4a2/printenv.png 606w&quot;
            sizes=&quot;(max-width: 606px) 100vw, 606px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/dc6b222732054f5bd81e1eb157928026/4d4a2/printenv.png&quot;
            alt=&quot;printenv&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;em&gt;Don&apos;t do this: it prints your secrets to the logs&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Reading them over SSH keeps them out of the logs. But it also means anyone who can rerun a job
with SSH can read your project&apos;s secrets, so treat that ability as sensitive.&lt;/p&gt;
&lt;h2 id=&quot;step-1-set-up-an-ssh-key&quot;&gt;Step 1: set up an SSH key&lt;/h2&gt;
&lt;p&gt;CircleCI authorises the SSH session with the keys on your GitHub (or Bitbucket) account. If you
already push to GitHub over SSH, you can skip to step 2.&lt;/p&gt;
&lt;p&gt;Check for an existing key:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ls&lt;/span&gt; ~/.ssh/id_ed25519.pub ~/.ssh/id_rsa.pub &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;/dev/null&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If there&apos;s none, create one. Ed25519 keys are shorter and faster than RSA, and are what GitHub
recommends:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; ed25519 &lt;span class=&quot;token parameter variable&quot;&gt;-C&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;you@example.com&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Press Enter to accept the default file (&lt;code class=&quot;language-text&quot;&gt;~/.ssh/id_ed25519&lt;/code&gt;), and choose a passphrase.&lt;/p&gt;
&lt;p&gt;Start the SSH agent and add the key. On macOS, &lt;code class=&quot;language-text&quot;&gt;--apple-use-keychain&lt;/code&gt; saves the passphrase in
your Keychain (it replaces the old &lt;code class=&quot;language-text&quot;&gt;-K&lt;/code&gt; flag):&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;ssh-agent &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;
ssh-add --apple-use-keychain ~/.ssh/id_ed25519   &lt;span class=&quot;token comment&quot;&gt;# macOS&lt;/span&gt;
ssh-add ~/.ssh/id_ed25519                        &lt;span class=&quot;token comment&quot;&gt;# Linux&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Copy the &lt;strong&gt;public&lt;/strong&gt; key:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pbcopy &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; ~/.ssh/id_ed25519.pub                   &lt;span class=&quot;token comment&quot;&gt;# macOS&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; ~/.ssh/id_ed25519.pub                        &lt;span class=&quot;token comment&quot;&gt;# anywhere: copy the output&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Add it to GitHub under &lt;strong&gt;Settings → SSH and GPG keys → New SSH key&lt;/strong&gt;, and check it works:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ssh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-T&lt;/span&gt; git@github.com&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;step-2-rerun-the-job-with-ssh&quot;&gt;Step 2: rerun the job with SSH&lt;/h2&gt;
&lt;p&gt;Open the failed (or any) job in CircleCI, open the &lt;strong&gt;Rerun&lt;/strong&gt; menu at the top right, and choose
&lt;strong&gt;Rerun job with SSH&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 794px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 43.90243902439025%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAABM0lEQVR42nWQXW6CQBSFZw8YE14w2lLkgbZMN9BEqnU3JO6ONZCmL428IARaNPwVQcBIMD0ZYlJq+j3cHO6cM/cyZDweTyYTkXHDQAf19gI0DFN5qi1exDtxNBo9MwRBIDzPwzGbzTRNk2UZSXRwETpwSJKE5IAbzBfzt493+kQ5jlutVrquD4dDcs94YEAoitJ1HhkKA4JSunxdUpVCq6qKT3jI1z8EQbDdbiE+Gb7vbzYbVOiAgSNyuiKKIpxZlrVer6HzPN/v94fDYbfbGYYRhqFt247jnM9nUvYpigIBOLppSZJkWRbHcZqmWMQ0TVyBmTiq65oUfWDtkjDB7Xkeatu2TdOgYhoWOR6PSMLcm1xVFcJd7DfhhW5h/BQ88P9dG2RXfF/Am+EtXNfF2mVZ/gAV/22yyVaD/wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/8c8e070bd005b3fb26ad013eca0a396a/ac387/run-with-ssh.webp 205w,
/static/8c8e070bd005b3fb26ad013eca0a396a/0cb6c/run-with-ssh.webp 410w,
/static/8c8e070bd005b3fb26ad013eca0a396a/ae710/run-with-ssh.webp 794w&quot;
              sizes=&quot;(max-width: 794px) 100vw, 794px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/8c8e070bd005b3fb26ad013eca0a396a/85860/run-with-ssh.png 205w,
/static/8c8e070bd005b3fb26ad013eca0a396a/d68e4/run-with-ssh.png 410w,
/static/8c8e070bd005b3fb26ad013eca0a396a/7de01/run-with-ssh.png 794w&quot;
            sizes=&quot;(max-width: 794px) 100vw, 794px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/8c8e070bd005b3fb26ad013eca0a396a/7de01/run-with-ssh.png&quot;
            alt=&quot;run-with-ssh&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;em&gt;Rerun job with SSH (older CircleCI interface; the option is in the same place today)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The job runs again with an extra &lt;strong&gt;Enable SSH&lt;/strong&gt; step. Expand it to find the connection command:&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 690px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 42.926829268292686%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAABGUlEQVR42pWQy1KDMBSG8w6MLmwLlVAIAZNwx2dkQRXGZV0Itk9YZ1BHf4IwHceN35w5+Tm3nEA+P97P57dhGL7+Dzn2/eH50HXdq6bXQBxHxsjpNB4vf0E8d0cpZcwLAu66O99nlNqbjWlZlmma6/UaYrW6uZq5voDESjLGwjAU4g6ecx4EgY8ZmvCHANNRgJRt23SGlEUupczzLEniKFKT59zXMM/zprql5/YCgqYsS9M0gRVFDg2PWbCyLDAIa2+3sN+MzfdloZSMNLNQQG+Niz0cyxZLBN5xHLxZ+JxFkUSnUgKGzfFIKUUcRxAwRPApxPhf8CLUOA41DIM8tW293zdN8zjTttAPjQYCkSmLuE6N1HVdVdU3WZC9bYVgtsYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/73d7c1b07df90f243e18e69571853a8b/ac387/logs.webp 205w,
/static/73d7c1b07df90f243e18e69571853a8b/0cb6c/logs.webp 410w,
/static/73d7c1b07df90f243e18e69571853a8b/8efd0/logs.webp 690w&quot;
              sizes=&quot;(max-width: 690px) 100vw, 690px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/73d7c1b07df90f243e18e69571853a8b/85860/logs.png 205w,
/static/73d7c1b07df90f243e18e69571853a8b/d68e4/logs.png 410w,
/static/73d7c1b07df90f243e18e69571853a8b/1e043/logs.png 690w&quot;
            sizes=&quot;(max-width: 690px) 100vw, 690px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/73d7c1b07df90f243e18e69571853a8b/1e043/logs.png&quot;
            alt=&quot;logs&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;em&gt;The Enable SSH step shows the host and port&lt;/em&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;You can now SSH into this box if your SSH public key is added:
    $ ssh -p 54782 3.80.12.34&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;step-3-connect&quot;&gt;Step 3: connect&lt;/h2&gt;
&lt;p&gt;Run the command from your terminal:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ssh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;54782&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3.80&lt;/span&gt;.12.34&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you have several keys and the connection is refused, point SSH at the right one:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ssh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; ~/.ssh/id_ed25519 &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;54782&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3.80&lt;/span&gt;.12.34&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;step-4-debug&quot;&gt;Step 4: debug&lt;/h2&gt;
&lt;p&gt;You&apos;re now in the same environment as the job, after its steps have run. Some useful starting
points:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; ~/project            &lt;span class=&quot;token comment&quot;&gt;# the default working directory for checked-out code&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-la&lt;/span&gt;                  &lt;span class=&quot;token comment&quot;&gt;# did checkout and the build produce what you expect?&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;printenv&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;         &lt;span class=&quot;token comment&quot;&gt;# environment variables (stays in your terminal, not the logs)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;# tool versions&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$BASH_ENV&lt;/span&gt;&quot;&lt;/span&gt;         &lt;span class=&quot;token comment&quot;&gt;# variables exported by earlier steps&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then rerun the command from the failing step and try fixes. Anything you change only lives in this
box, so copy working fixes back into your code or &lt;code class=&quot;language-text&quot;&gt;.circleci/config.yml&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;step-5-end-the-session&quot;&gt;Step 5: end the session&lt;/h2&gt;
&lt;p&gt;The SSH job keeps running until you disconnect and it times out, and it uses credits while it runs.
When you&apos;re done:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then &lt;strong&gt;cancel the job&lt;/strong&gt; in the CircleCI UI so it doesn&apos;t keep running. See CircleCI&apos;s
&lt;a href=&quot;https://circleci.com/docs/ssh-access-jobs/&quot;&gt;debugging with SSH&lt;/a&gt; docs for the current time limits.&lt;/p&gt;
&lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Permission denied (publickey)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The key on this machine isn&apos;t on the GitHub/Bitbucket account you use with CircleCI. Check &lt;code class=&quot;language-text&quot;&gt;ssh -T git@github.com&lt;/code&gt;, or pass the key with &lt;code class=&quot;language-text&quot;&gt;-i&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;Too many authentication failures&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SSH tried other keys first. Use &lt;code class=&quot;language-text&quot;&gt;ssh -i &amp;lt;key&gt; -o IdentitiesOnly=yes -p &amp;lt;port&gt; &amp;lt;ip&gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No &lt;strong&gt;Rerun job with SSH&lt;/strong&gt; option&lt;/td&gt;
&lt;td&gt;You need permission to rerun jobs on the project; ask an admin of your CircleCI organisation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The command from a step isn&apos;t found&lt;/td&gt;
&lt;td&gt;You may be in a different shell. Check &lt;code class=&quot;language-text&quot;&gt;echo $SHELL&lt;/code&gt; and the &lt;code class=&quot;language-text&quot;&gt;shell:&lt;/code&gt; in your config&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</content:encoded></item></channel></rss>