<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anton Yakutovich</title>
    <description>The latest articles on DEV Community by Anton Yakutovich (@drakulavich).</description>
    <link>https://dev.to/drakulavich</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F678151%2F5476dba3-dc6b-4095-8bd0-6070f8b8d6fe.jpeg</url>
      <title>DEV Community: Anton Yakutovich</title>
      <link>https://dev.to/drakulavich</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/drakulavich"/>
    <language>en</language>
    <item>
      <title>Polly wants a transcript: giving agents ears and a voice, on your own machine</title>
      <dc:creator>Anton Yakutovich</dc:creator>
      <pubDate>Sun, 31 May 2026 13:11:25 +0000</pubDate>
      <link>https://dev.to/drakulavich/polly-wants-a-transcript-giving-agents-ears-and-a-voice-on-your-own-machine-165</link>
      <guid>https://dev.to/drakulavich/polly-wants-a-transcript-giving-agents-ears-and-a-voice-on-your-own-machine-165</guid>
      <description>&lt;p&gt;Half the messages I send my coding agents these days start life as a voice note. I'm walking the dog, an idea lands, I mumble it into my phone, and later something turns it into text an agent can actually act on. It's a great workflow — right up until you notice &lt;em&gt;where&lt;/em&gt; the audio goes to become text.&lt;/p&gt;

&lt;p&gt;Because the default answer to "transcribe this" is still: ship it to someone's cloud. Whisper API, AWS Transcribe, Deepgram, Google Speech-to-Text. Your voice — which is about as personal as data gets — leaves the building, runs through &lt;em&gt;their&lt;/em&gt; model, on &lt;em&gt;their&lt;/em&gt; meter, under a privacy policy they can rewrite on a Tuesday. And when you want the round trip — text &lt;em&gt;back&lt;/em&gt; to speech — it's the same story: AWS Polly, ElevenLabs, another key, another bill.&lt;/p&gt;

&lt;p&gt;Meanwhile my laptop has a Neural Engine sitting mostly idle. Whisper-class models run locally just fine now. So why is the audio leaving at all?&lt;/p&gt;

&lt;p&gt;That itch turned into &lt;a href="https://github.com/drakulavich/kesha-voice-kit" rel="noopener noreferrer"&gt;Kesha Voice Kit&lt;/a&gt; 🦜 — and Polly can keep her transcript.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Kesha (yes, named after &lt;a href="https://en.wikipedia.org/wiki/The_Return_of_the_Prodigal_Parrot" rel="noopener noreferrer"&gt;the cartoon parrot&lt;/a&gt; — &lt;em&gt;"Свободу попугаям!"&lt;/em&gt; is literally the demo clip) is a local-first voice toolkit: &lt;strong&gt;speech-to-text and back&lt;/strong&gt;, no cloud, no account, no API key. The whole thing is one ~20 MB Rust binary — no Python, no &lt;code&gt;ffmpeg&lt;/code&gt;, no native Node addons to babysit.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transcribe&lt;/strong&gt; in 25 languages — up to &lt;strong&gt;~19× faster than Whisper&lt;/strong&gt; on Apple Silicon, ~2.5× on CPU&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speak back&lt;/strong&gt; in 9 languages, auto-picking a voice from the text's language&lt;/li&gt;
&lt;li&gt;On a Mac it runs on the &lt;strong&gt;Apple Neural Engine&lt;/strong&gt; via CoreML; everywhere else it falls back to ONNX&lt;/li&gt;
&lt;li&gt;Models are &lt;strong&gt;never auto-downloaded&lt;/strong&gt; — you ask for them once, explicitly, and every weight is pinned by SHA-256 so a mirror can't quietly swap it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CLI is a thin &lt;a href="https://bun.sh" rel="noopener noreferrer"&gt;Bun&lt;/a&gt; wrapper; the engine is the Rust binary it shells out to. Pipe-friendly by design — transcript on stdout, errors on stderr.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add &lt;span class="nt"&gt;-g&lt;/span&gt; @drakulavich/kesha-voice-kit
kesha &lt;span class="nb"&gt;install&lt;/span&gt;                 &lt;span class="c"&gt;# downloads engine + models (explicit — never automatic)&lt;/span&gt;

kesha audio.ogg               &lt;span class="c"&gt;# → transcript to stdout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want it to talk?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kesha &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--tts&lt;/span&gt;           &lt;span class="c"&gt;# opt-in voices (~990 MB)&lt;/span&gt;
kesha say &lt;span class="s2"&gt;"Свободу попугаям!"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; freedom.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, no region to pick, no spend alert to set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new in 1.22.0
&lt;/h2&gt;

&lt;p&gt;The release I just cut adds two things I'd wanted for a while.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multilingual voices.&lt;/strong&gt; Text-to-speech used to be English + Russian and not much else. 1.22.0 wires up the multilingual &lt;a href="https://huggingface.co/hexgrad/Kokoro-82M" rel="noopener noreferrer"&gt;Kokoro&lt;/a&gt; voices on Apple Silicon, so &lt;code&gt;kesha say&lt;/code&gt; now covers Spanish, French, Italian, Portuguese and more — all on the Neural Engine, all offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stable error codes everywhere.&lt;/strong&gt; Every failure path now prints a machine-readable line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error [E_MODEL_MISSING]: TTS models not installed. Run: kesha install --tts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kesha --error-codes-json&lt;/code&gt; dumps the whole taxonomy. If you're driving Kesha from a script or an agent, you no longer have to grep prose to find out &lt;em&gt;why&lt;/em&gt; it bailed. There's even a test that fails CI if a code exists in the binary but not in the docs — drift is a build break, not a surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gotcha I'll save you from (and why I made it louder)
&lt;/h2&gt;

&lt;p&gt;Here's the kind of thing that only shows up once real users point it at real text.&lt;/p&gt;

&lt;p&gt;I added the multilingual voices, ran &lt;code&gt;kesha say&lt;/code&gt; on a line of Hindi… and got &lt;strong&gt;noise&lt;/strong&gt;. Not wrong words — actual garbage audio. No error, no warning. The most confident kind of broken.&lt;/p&gt;

&lt;p&gt;The root cause is buried a layer down. The on-device Kokoro path phonemizes text with an &lt;strong&gt;English-only&lt;/strong&gt; grapheme-to-phoneme model. Feed it Latin script and it's happy. Feed it Devanagari, kana, or Han characters and it doesn't &lt;em&gt;fail&lt;/em&gt; — it just produces phonemes that mean nothing, and the model dutifully sings them.&lt;/p&gt;

&lt;p&gt;I had three options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Emit the garbage audio&lt;/td&gt;
&lt;td&gt;No. Confidently wrong is the worst failure mode there is.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quietly transliterate to Latin and guess&lt;/td&gt;
&lt;td&gt;Fragile, surprising, hides the real gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refuse with a clear, coded error&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So now non-Latin text aimed at a Latin-only voice stops immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error [E_SCRIPT_UNSUPPORTED]: voice 'hi' cannot phonemize Devanagari text;
it only supports Latin-script input. Romanize the text, or use a voice
whose engine supports Devanagari.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 4, a stable code, an actionable hint. &lt;strong&gt;Fail fast beats fail quietly, every single time&lt;/strong&gt; — especially for an agent that can't &lt;em&gt;hear&lt;/em&gt; that the WAV it got back is nonsense. Real multilingual G2P for those scripts is &lt;a href="https://github.com/drakulavich/kesha-voice-kit/issues/492" rel="noopener noreferrer"&gt;tracked as an open issue&lt;/a&gt;; until it lands, the tool tells you the truth instead of humming gibberish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugging it into agents
&lt;/h2&gt;

&lt;p&gt;The reason any of this exists is that I wanted my agents to hear and speak without phoning home. So Kesha speaks the protocols they do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP&lt;/strong&gt; — &lt;code&gt;kesha mcp&lt;/code&gt; exposes transcribe / synthesize / list-voices as tools to any MCP client (Claude, Cursor, Codex, Gemini)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/drakulavich/kesha-voice-kit/blob/main/docs/openclaw.md" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt;&lt;/strong&gt; — drop-in skill so your agent grows ears&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/drakulavich/kesha-voice-kit/blob/main/docs/hermes.md" rel="noopener noreferrer"&gt;Hermes&lt;/a&gt;&lt;/strong&gt; — local STT/TTS through command providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raycast&lt;/strong&gt; on macOS, and a programmatic &lt;code&gt;@drakulavich/kesha-voice-kit/core&lt;/code&gt; API if you'd rather call it from a Bun program&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A voice note in, a transcript out, an answer spoken back — and the audio never left the laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest about the edges
&lt;/h2&gt;

&lt;p&gt;It's not magic. Diarization and the multilingual voices are Apple-Silicon-only today (Linux/Windows get a clear error, not a crash). The first TTS download is ~990 MB — local models aren't free, they're just &lt;em&gt;yours&lt;/em&gt;. And as above, true G2P for non-Latin scripts isn't here yet. I'd rather ship the limitation with a loud error than paper over it.&lt;/p&gt;

&lt;p&gt;It's MIT, it's on &lt;a href="https://github.com/drakulavich/kesha-voice-kit" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and &lt;a href="https://www.npmjs.com/package/@drakulavich/kesha-voice-kit" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, and &lt;code&gt;bun add -g @drakulavich/kesha-voice-kit&lt;/code&gt; is the whole install.&lt;/p&gt;

&lt;p&gt;What does your local-first setup look like these days — and what's still quietly phoning home in your stack that you wish wasn't? 🦜&lt;/p&gt;

</description>
      <category>agents</category>
      <category>rust</category>
      <category>cli</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Awk! Awk! Add a diagram!: Greptile-style PR diagrams, minus the SaaS</title>
      <dc:creator>Anton Yakutovich</dc:creator>
      <pubDate>Sun, 31 May 2026 12:18:58 +0000</pubDate>
      <link>https://dev.to/drakulavich/awk-awk-add-a-diagram-greptile-style-pr-diagrams-minus-the-saas-2hni</link>
      <guid>https://dev.to/drakulavich/awk-awk-add-a-diagram-greptile-style-pr-diagrams-minus-the-saas-2hni</guid>
      <description>&lt;p&gt;I review a lot of pull requests with Claude Code and Codex these days. The &lt;code&gt;/review&lt;/code&gt; command is genuinely good — it reads the diff, spots the sharp edges, writes it up. But the output is always a wall of prose. On a PR that touches a request path across three services, I'd find myself drawing the call flow on a napkin to convince myself the review was right.&lt;/p&gt;

&lt;p&gt;Meanwhile, the hosted reviewers — Greptile, CodeRabbit — just &lt;em&gt;draw it for you&lt;/em&gt;. Every PR gets a tidy Mermaid sequence or flow diagram at the top of the review. It's lovely. I wanted that.&lt;/p&gt;

&lt;p&gt;So I looked at what it would take.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's notable here?
&lt;/h2&gt;

&lt;p&gt;The hosted tools render those diagrams server-side: your diff goes to a third party, runs through &lt;em&gt;their&lt;/em&gt; LLM, on &lt;em&gt;their&lt;/em&gt; subscription. That's a lot of machinery for "turn this diff into a picture" — especially when my &lt;code&gt;/review&lt;/code&gt; is &lt;em&gt;already&lt;/em&gt; being written by a perfectly capable LLM running on my machine. The model that just understood the change well enough to critique it can obviously draw it.&lt;/p&gt;

&lt;p&gt;The diagram doesn't need a SaaS. It needs about thirty lines of glue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: let the agent draw, let &lt;code&gt;gh&lt;/code&gt; post
&lt;/h2&gt;

&lt;p&gt;That's the whole design of &lt;a href="https://github.com/drakulavich/iago" rel="noopener noreferrer"&gt;Iago&lt;/a&gt; 🦜 — a skill, not a service:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The host agent draws the diagram.&lt;/strong&gt; Iago's &lt;code&gt;SKILL.md&lt;/code&gt; tells your agent which diagram type fits the diff and how to write valid Mermaid. No provider SDK, no API key — it uses the LLM you're already paying for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tiny helper posts it.&lt;/strong&gt; A single TypeScript file (&lt;code&gt;post.ts&lt;/code&gt;, run by &lt;code&gt;bun&lt;/code&gt;) finds your &lt;code&gt;/review&lt;/code&gt; comment and appends the diagram via authenticated &lt;code&gt;gh&lt;/code&gt;. That's the entire runtime: &lt;code&gt;bun&lt;/code&gt; + &lt;code&gt;gh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's idempotent.&lt;/strong&gt; Re-run it and it replaces its own block instead of stacking duplicates, using a pair of HTML markers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No server. No key. No diff leaves your machine except through the same &lt;code&gt;gh&lt;/code&gt; you already trust.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2sdw1mvzzww8d7zchz9p.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2sdw1mvzzww8d7zchz9p.gif" alt="Iago installs into every agent in one command" width="799" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually posts
&lt;/h2&gt;

&lt;p&gt;Iago auto-detects the diagram type from the diff — and you can always override it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The diff looks like…&lt;/th&gt;
&lt;th&gt;You get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;migrations / &lt;code&gt;*.sql&lt;/code&gt; / ORM models&lt;/td&gt;
&lt;td&gt;an &lt;strong&gt;entity-relation&lt;/strong&gt; diagram&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;new classes / interfaces across files&lt;/td&gt;
&lt;td&gt;a &lt;strong&gt;class&lt;/strong&gt; diagram&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a request hopping handler → client → worker&lt;/td&gt;
&lt;td&gt;a &lt;strong&gt;sequence&lt;/strong&gt; diagram&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;branching / state-machine logic&lt;/td&gt;
&lt;td&gt;a &lt;strong&gt;flowchart&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a docs or dependency bump&lt;/td&gt;
&lt;td&gt;nothing — it abstains&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;See an example:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwtb6w48mqrxnrjxv0k3r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwtb6w48mqrxnrjxv0k3r.png" alt=" " width="800" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub renders that inline, right on top of the review. (dev.to may show the source above; paste it into a PR to see it drawn.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;

&lt;p&gt;Runtime is just &lt;code&gt;bun&lt;/code&gt; and an authenticated &lt;code&gt;gh&lt;/code&gt;. Install the skill into whatever agent you use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx @drakulavich/iago &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Claude Code you can grab the plugin instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add drakulavich/iago
/plugin install iago@iago-marketplace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, on any PR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/review        # your agent writes the review
/iago          # Iago perches on top of it and squawks a diagram
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/squawk&lt;/code&gt; is an alias, because of course it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gotcha I'll save you from
&lt;/h2&gt;

&lt;p&gt;Here's the kind of thing that only bites you in production. My sequence-diagram example stopped rendering on GitHub one day, with no useful error. The culprit:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;loop&lt;/code&gt; is a reserved keyword in Mermaid's sequence grammar — along with &lt;code&gt;alt&lt;/code&gt;, &lt;code&gt;opt&lt;/code&gt;, &lt;code&gt;par&lt;/code&gt;, &lt;code&gt;note&lt;/code&gt;, &lt;code&gt;end&lt;/code&gt;, and &lt;code&gt;activate&lt;/code&gt;. Name a participant &lt;code&gt;Loop&lt;/code&gt; and GitHub's renderer silently chokes. Renaming it to &lt;code&gt;Reader&lt;/code&gt; fixed it instantly.&lt;/p&gt;

&lt;p&gt;So Iago's templates steer the agent away from those reserved ids, and the helper validates with &lt;code&gt;mermaid.parse()&lt;/code&gt; before anything gets posted. You shouldn't ever hit it — but if you write Mermaid by hand, now you know where the body is buried. 💀&lt;/p&gt;

&lt;h2&gt;
  
  
  "Okay, but how do you know the diagrams are any good?"
&lt;/h2&gt;

&lt;p&gt;Fair question, and I'll be honest: there's no benchmark. Diagram quality is the kind of thing a number doesn't capture well, so I validate it by dogfooding — running &lt;code&gt;/iago&lt;/code&gt; on real PRs whenever I touch the selection rubric, and looking at what comes out. A formal eval harness felt like overkill for a tool whose whole pitch is "small and dependency-free." I'd rather keep it that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your next PR
&lt;/h2&gt;

&lt;p&gt;If your &lt;code&gt;/review&lt;/code&gt; workflow ends in a wall of text, this is a thirty-second add-on that puts a picture on top of it — without signing up for anything. It's MIT, it's on &lt;a href="https://github.com/drakulavich/iago" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and &lt;a href="https://www.npmjs.com/package/@drakulavich/iago" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What does your AI code-review setup look like these days — and what would &lt;em&gt;you&lt;/em&gt; want drawn automatically on every PR? Would love to hear what you're running. 💎&lt;/p&gt;

</description>
      <category>github</category>
      <category>codereview</category>
      <category>agentskills</category>
    </item>
    <item>
      <title>Fast and reliable end-to-end tests with Playwright on GitHub Actions</title>
      <dc:creator>Anton Yakutovich</dc:creator>
      <pubDate>Tue, 24 Jan 2023 17:48:30 +0000</pubDate>
      <link>https://dev.to/drakulavich/fast-and-reliable-end-to-end-tests-with-playwright-on-github-actions-2mkh</link>
      <guid>https://dev.to/drakulavich/fast-and-reliable-end-to-end-tests-with-playwright-on-github-actions-2mkh</guid>
      <description>&lt;p&gt;&lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; is a powerful web testing tool supporting Chromium, Firefox and WebKit engines. The project has excellent documentation and many examples.&lt;/p&gt;

&lt;p&gt;I use Playwright for E2E tests on the DEV environment. Today I'm going to show how you can integrate Playwright on GitHub Actions more efficiently.&lt;/p&gt;

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

&lt;p&gt;The documentation &lt;a href="https://playwright.dev/docs/ci#github-actions" rel="noopener noreferrer"&gt;suggests&lt;/a&gt; using the following snippet to run Playwright inside GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;18'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Playwright&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright install --with-deps&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run your tests&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload test results&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v3&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playwright-report&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playwright-report&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is notable here?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's supposed to run all tests on the same type of GitHub Runner. &lt;code&gt;ubuntu-latest&lt;/code&gt; by default.&lt;/li&gt;
&lt;li&gt;All supported browser engines (chromium, firefox, WebKit) will be installed on the Linux runner.&lt;/li&gt;
&lt;li&gt;Playwright will execute tests for all browser engines from the same runner.&lt;/li&gt;
&lt;li&gt;The last step will prepare an archive with an HTML report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After several months of experiments I come up with the following configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🧪 ${{ matrix.project }} E2E Tests&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.os }}&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chromium&lt;/span&gt;
            &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
            &lt;span class="na"&gt;cache_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.cache/ms-playwright&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;firefox&lt;/span&gt;
            &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
            &lt;span class="na"&gt;cache_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.cache/ms-playwright&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webkit&lt;/span&gt;
            &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-12&lt;/span&gt;
            &lt;span class="na"&gt;cache_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/Library/Caches/ms-playwright&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node-version-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.nvmrc'&lt;/span&gt;
        &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;⚡️ Cache NPM dependencies&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v3&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cache-primes&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node_modules&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;⚡️ Cache playwright binaries&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v3&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playwright-cache&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.cache_dir }}&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-${{ matrix.project }}-pw-${{ hashFiles('**/.playwright-version') }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;📥 Install deps&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.cache-primes.outputs.cache-hit != 'true'&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;make install&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;📥 Install ${{ matrix.project }} with Playwright&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.playwright-cache.outputs.cache-hit != 'true'&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright install --with-deps ${{ matrix.project }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🎭 Playwright tests&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test --project=${{ matrix.project }}&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;DEBUG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pw:api,pw:browser*&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;📊 Upload test results&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v3&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playwright-report-${{ matrix.project }}&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playwright-report&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I've changed in GitHub Workflow for Playwright tests?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I use a matrix with three browser engines to run the jobs simultaneously. Instead of &lt;code&gt;ubuntu-latest&lt;/code&gt;, I run WebKit tests on &lt;code&gt;macos-12&lt;/code&gt; runner. I had to make the change to avoid fragile tests. I noticed that the &lt;code&gt;page.goto()&lt;/code&gt; API works differently on Linux and macOS for the WebKit engine. WebKit didn't wait for &lt;code&gt;load&lt;/code&gt; event, which causes failures from time to time. At the same time, I've never caught such a problem locally on my MacBook. If you're interested, &lt;a href="https://github.com/microsoft/playwright/issues/8340" rel="noopener noreferrer"&gt;the ticket&lt;/a&gt; is still open in the Playwright bug tracker.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;fail-fast&lt;/code&gt; parameter is set to false to wait for all testing jobs to complete, even if one of them fails for some reason.&lt;/li&gt;
&lt;li&gt;I believe it is essential to specify &lt;code&gt;timeout-minutes&lt;/code&gt; for E2E tests so that the tests will crash if the infrastructure hangs or behaves incorrectly.&lt;/li&gt;
&lt;li&gt;I use the &lt;em&gt;.nvmrc&lt;/em&gt; file inside the repository to specify the NodeJS version. DRY all over the place!&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/drakulavich/aggressive-dependency-caching-in-github-actions-3c64"&gt;&lt;strong&gt;Configured aggressive cache&lt;/strong&gt;&lt;/a&gt; for npm dependencies and binary files with browsers. I added a special target to Makefile to calculate the Playwright browsers hash:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;bump-playwright&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
  &lt;span class="err"&gt;npm&lt;/span&gt; &lt;span class="err"&gt;install&lt;/span&gt; &lt;span class="err"&gt;--save-dev&lt;/span&gt; &lt;span class="err"&gt;@playwright/test&lt;/span&gt;
  &lt;span class="err"&gt;npx&lt;/span&gt; &lt;span class="err"&gt;playwright&lt;/span&gt; &lt;span class="err"&gt;--version&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.github/.playwright-version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After executing &lt;code&gt;make bump-playwright&lt;/code&gt; command, the latest PW version will be installed and the value of the installed release will be written to the &lt;em&gt;.github/.playwright-version&lt;/em&gt; file. GitHub Actions calculate the hash for the cache from the file content. Example file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .github/.playwright-version
Version 1.29.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Each runner installs only one browser and it's pretty handy cause the HTML report will have the results for that browser — easy to navigate and spot the issues. This distinction also allows you to have an idea of the test execution speed in each browser and to notice flickering tests specific to a particular engine.&lt;/li&gt;
&lt;li&gt;Playwright tests run with enabled DEBUG, so that in case of a crash, you can quickly get additional information for diagnostics.&lt;/li&gt;
&lt;li&gt;In contrast to the recommended configuration from the documentation, I prefer to upload artifacts with the report only in case of test failures. I don't see the point in storing those artifacts on the cloud, given how often the tests are run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I shared my findings for configuring E2E playwright tests in GitHub Actions. Feel free to ask questions about the example GitHub workflow.&lt;/p&gt;

&lt;p&gt;What Playwright tricks do you use in your daily work? Would love to hear about your hidden gems 💎.&lt;/p&gt;

&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/photos/JySoEnr-eOg" rel="noopener noreferrer"&gt;Denny Müller&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>github</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Aggressive dependency caching in GitHub Actions</title>
      <dc:creator>Anton Yakutovich</dc:creator>
      <pubDate>Tue, 18 Oct 2022 15:54:52 +0000</pubDate>
      <link>https://dev.to/drakulavich/aggressive-dependency-caching-in-github-actions-3c64</link>
      <guid>https://dev.to/drakulavich/aggressive-dependency-caching-in-github-actions-3c64</guid>
      <description>&lt;p&gt;There are three things you can watch forever: fire burning, water falling, and how the build passes the stages in Pipeline after the next commit. To make the wait less tedious, it's best to take care of the CI setup from the beginning.&lt;/p&gt;

&lt;p&gt;GitHub Actions has a cache that gets to the runner's virtual machine in seconds. In this article I'd like to share examples of how to set up aggressive dependency caching. Why did I call this approach "aggressive caching"? Because we will be caching not only the packages archives but also the state of the environment after installation.&lt;/p&gt;

&lt;p&gt;For Node.js it will be the &lt;code&gt;node_modules&lt;/code&gt; directory, and for Python it will be the virtualenv directory with installed dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node.js Example
&lt;/h2&gt;

&lt;p&gt;Let's take the typical setup for dependency caching example mentioned in the documentation. If you don't need any exotics, you can use the standard &lt;code&gt;actions/setup-node&lt;/code&gt; &lt;a href="https://github.com/actions/setup-node#caching-global-packages-data" rel="noopener noreferrer"&gt;action&lt;/a&gt;, specifying a package manager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will save the &lt;code&gt;.npm&lt;/code&gt; directory with the global package cache. Sounds great! Remember that if we have several workflow jobs requiring &lt;code&gt;npm ci&lt;/code&gt; inside, this will also be time-consuming.&lt;/p&gt;

&lt;p&gt;Let's imagine a pipelining with several jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         │
          Create         │                        Reuse
          Dependencies   │                        Dependencies
          Cache          │
                         │  ┌────────────────────┐
                ┌────────┼──►     Lint  Job      ├────────────────────┐
                │        │  └────────────────────┘                    │
                │        │                                            │
                │        │                                            │
┌───────────────┴────┐   │  ┌────────────────────┐                 ┌──▼─────────────────┐
│     Build Job      ├───┼──►     Test  Job      ├─────────────────►     Deploy Job     │
└───────────────┬────┘   │  └────────────────────┘                 └──▲─────────────────┘
                │        │                                            │
                │        │                                            │
                │        │  ┌────────────────────┐                    │
                └────────┼──►     E2E   Job      ├────────────────────┘
                         │  └────────────────────┘
                         │
                         │
                         │
                         │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ideally, we want to install dependencies only in the first job and get a state with available dependencies in all subsequent jobs.&lt;br&gt;
I'll show how to achieve it using a sample repo — &lt;a href="https://github.com/drakulavich/ts-redux-react-realworld-example-app" rel="noopener noreferrer"&gt;redux-react-realworld-example-app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpip2fqk7w7so3zucq3v5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpip2fqk7w7so3zucq3v5.png" alt="Node.js CI workflow on GitHub Actions" width="800" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first (&lt;code&gt;build&lt;/code&gt;) job might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;node-version-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.nvmrc'&lt;/span&gt; &lt;span class="c1"&gt;# (1)&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache NPM dependencies&lt;/span&gt; &lt;span class="c1"&gt;# (2)&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v3&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cache-primes&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node_modules&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt; &lt;span class="c1"&gt;# (3)&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.cache-primes.outputs.cache-hit != 'true'&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line #1 specifies the node version using &lt;code&gt;.nvmrc&lt;/code&gt; file. That's the alternative way to specify the version and it helps follow the DRY - Don't Repeat Yourself principle.&lt;/p&gt;

&lt;p&gt;In line #2 we use &lt;code&gt;actions/cache&lt;/code&gt; to cache the &lt;code&gt;node_modules&lt;/code&gt; directory. We use the hash from the &lt;code&gt;package-lock.json&lt;/code&gt; file as the key.&lt;/p&gt;

&lt;p&gt;In line #3 we only install dependencies if the cache is invalidated.&lt;/p&gt;

&lt;p&gt;To automatically retrieve &lt;code&gt;node_modules&lt;/code&gt; in subsequent jobs, you must declare &lt;code&gt;actions/cache&lt;/code&gt; with &lt;strong&gt;the same key&lt;/strong&gt;. For example the &lt;code&gt;test&lt;/code&gt; job can be configured as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;node-version-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.nvmrc'&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache NPM dependencies&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node_modules&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}&lt;/span&gt; &lt;span class="c1"&gt;# (1)&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test&lt;/span&gt; &lt;span class="c1"&gt;# (2)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line #1 specifies the cache key. The key must be the same as in the &lt;code&gt;build&lt;/code&gt; job. After the &lt;code&gt;actions/cache&lt;/code&gt; step we consider that the dependencies are installed and run the tests in line #2.&lt;/p&gt;

&lt;p&gt;Check out the complete &lt;a href="https://github.com/drakulavich/ts-redux-react-realworld-example-app/blob/main/.github/workflows/pipeline.yml" rel="noopener noreferrer"&gt;workflow on GitHub&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Standard scenario from &lt;code&gt;actions/setup-python&lt;/code&gt; &lt;a href="https://github.com/actions/setup-python#caching-packages-dependencies" rel="noopener noreferrer"&gt;docs&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.9'&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pip'&lt;/span&gt; &lt;span class="c1"&gt;# caching pip dependencies&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This workflow will cache pip packages in &lt;code&gt;~/.cache/pip&lt;/code&gt;, but the installation step will always be performed, as in the previous example with &lt;code&gt;npm ci&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's see how we can optimize the installation of dependencies. I'll use the Django-based &lt;a href="https://github.com/tough-dev-school/education-backend" rel="noopener noreferrer"&gt;education-backend&lt;/a&gt; repo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Farn91ubhnzb0vv4i2jht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Farn91ubhnzb0vv4i2jht.png" alt="Python CI workflow on GitHub Actions" width="798" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's dive into the &lt;code&gt;build&lt;/code&gt; job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v4&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;setup-python&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;python-version-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.python-version'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;venv&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-venv-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/*requirements.txt') }}&lt;/span&gt; &lt;span class="c1"&gt;# (1)&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt; &lt;span class="c1"&gt;# (2)&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.cache-primes.outputs.cache-hit != 'true'&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;python -m venv venv&lt;/span&gt;
    &lt;span class="s"&gt;. venv/bin/activate&lt;/span&gt;
    &lt;span class="s"&gt;pip install --upgrade pip pip-tools&lt;/span&gt;
    &lt;span class="s"&gt;pip-sync requirements.txt dev-requirements.txt&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run the linter&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;. venv/bin/activate # (3)&lt;/span&gt;
    &lt;span class="s"&gt;cp src/app/.env.ci src/app/.env&lt;/span&gt;
    &lt;span class="s"&gt;make lint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we use the same idea for caching as for the Node.js project. There are some minor changes which are quite important. We need to specify the cache key for each python version involved in the workflow. Line #1 has &lt;code&gt;steps.setup-python.outputs.python-version&lt;/code&gt; variable exactly for this purpose.&lt;/p&gt;

&lt;p&gt;Dependencies installation from line #2 is tricky. For python we use a virtual environment created with the module &lt;code&gt;venv&lt;/code&gt;. The environment directory &lt;code&gt;venv&lt;/code&gt; will be cached. You can think about it as &lt;code&gt;node_modules&lt;/code&gt; for node.&lt;/p&gt;

&lt;p&gt;Line #3 has one more trick. After the cache is warmed up it's necessary to initialize virtualenv in the future steps. Otherwise, the python interpreter will not be able to detect the necessary libraries to import.&lt;/p&gt;

&lt;p&gt;The simplified &lt;code&gt;test&lt;/code&gt; job may look as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v4&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;setup-python&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;python-version-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.python-version'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;venv&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-venv-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/*requirements.txt') }}&lt;/span&gt; &lt;span class="c1"&gt;# (1)&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run the tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;. venv/bin/activate # (2)&lt;/span&gt;
    &lt;span class="s"&gt;cp src/app/.env.ci src/app/.env&lt;/span&gt;
    &lt;span class="s"&gt;make test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensure you use the same key for the caching step (line #1) and remember to activate the virtual environment before running the tests (line #2).&lt;/p&gt;

&lt;p&gt;Check out the complete &lt;a href="https://github.com/tough-dev-school/education-backend/blob/master/.github/workflows/ci.yml" rel="noopener noreferrer"&gt;workflow on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We've practiced "aggressive caching" with Node.js and Python examples. As far as you have a significant number of dependencies the changes can speed up your GitHub workflow sensibly. I recommend trying to set up workflows for your project using the references I've mentioned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/drakulavich/ts-redux-react-realworld-example-app/blob/main/.github/workflows/pipeline.yml" rel="noopener noreferrer"&gt;Node.js CI Workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tough-dev-school/education-backend/blob/master/.github/workflows/ci.yml" rel="noopener noreferrer"&gt;Python CI Workflow&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you still have questions about caching in GitHub Actions, don't hesitate to ask in the comments. I'll try to help.&lt;br&gt;
I would be grateful if you share your tips on how to speed up workflows on GitHub too.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>node</category>
      <category>python</category>
    </item>
    <item>
      <title>Operation Pact or: How I Learned to Stop Worrying and Love Contract Testing</title>
      <dc:creator>Anton Yakutovich</dc:creator>
      <pubDate>Tue, 20 Sep 2022 08:44:37 +0000</pubDate>
      <link>https://dev.to/drakulavich/operation-pact-or-how-i-learned-to-stop-worrying-and-love-contract-testing-4nhh</link>
      <guid>https://dev.to/drakulavich/operation-pact-or-how-i-learned-to-stop-worrying-and-love-contract-testing-4nhh</guid>
      <description>&lt;p&gt;I heard about Contract Tests in detail at the Ministry of Testing Meetup. Abhi Nandan made an excellent introduction talk &lt;a href="https://youtu.be/PWM8iLqpVdA?t=129" rel="noopener noreferrer"&gt;Contract testing in Polyglot Microservices&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I liked the idea and decided to try this approach on my current project. I must say that the devil is in the details. In theory, everything is simple until you try the integration in practice on an existing project.&lt;br&gt;
It is the same with algorithms: when you are watching how the developer on YouTube solves the task, everything seems straightforward. You immediately hit a snag while sitting down to solve the problem alone.&lt;/p&gt;

&lt;p&gt;Today I want to focus on practical examples, as much as possible close to real-world conditions. I encourage you to try it on your own to gain experience and see if these approaches are worth applying to your projects. We will use the &lt;a href="https://github.com/gothinkster/realworld" rel="noopener noreferrer"&gt;realword&lt;/a&gt; project on GitHub, an Exemplary Medium.com clone powered by different frameworks and tools.&lt;/p&gt;

&lt;p&gt;I like using this project for the demo because it's much more complex than &lt;code&gt;HelloWorld&lt;/code&gt;, where everything works most of the time. True-to-life problems give your extensive experience.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is so unique in contract tests?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://docs.pact.io/" rel="noopener noreferrer"&gt;Contract tests&lt;/a&gt; assert that inter-application messages conform to a shared understanding documented in a contract. Without contract testing, the only way to ensure that applications will work correctly together is by using expensive and brittle integration tests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can hear opinions: Contract tests often make sense when you have a microservice architecture with 10-50 services.&lt;br&gt;
I'm afraid I have to disagree with that. If you have at least one Provider (back-end with API) and Consumer (front-end), it already makes sense to try this testing approach.&lt;/p&gt;

&lt;p&gt;I suggest starting exploration with &lt;strong&gt;bi-directional contract tests&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://docs.pactflow.io/docs/bi-directional-contract-testing/" rel="noopener noreferrer"&gt;Bi-Directional Contract Testing&lt;/a&gt; is a type of &lt;strong&gt;static contract testing&lt;/strong&gt;.&lt;br&gt;
Teams generate a consumer contract from a mocking tool (such as Pact or Wiremock) and API providers verify a provider contract (such as an OAS) using a functional API testing tool (such as Postman). Pactflow then statically compares the contracts down to the field level to ensure they remain compatible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why is it better to start with BDCT? Bi-Directional Contract Testing (BDCT) allows you to use existing Provider Open API. If your current project has a back-end service with Open API specification, you are lucky to leverage it as a Provider contract.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Provider Contract
&lt;/h2&gt;

&lt;p&gt;Let's configure publishing of &lt;strong&gt;&lt;a href="https://github.com/gothinkster/realworld/blob/main/api/openapi.yml" rel="noopener noreferrer"&gt;OAS spec&lt;/a&gt;&lt;/strong&gt; from &lt;code&gt;realworld&lt;/code&gt; app to pactflow.&lt;/p&gt;

&lt;p&gt;Requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a &lt;a href="https://pactflow.io/try-for-free/" rel="noopener noreferrer"&gt;free account&lt;/a&gt; on Pactflow.io;&lt;/li&gt;
&lt;li&gt;Install &lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; on your machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We want to be able to publish Open API spec to Pactflow locally and from CI/CD. To do that we will need the following: API key from Pactflow, &lt;code&gt;docker&lt;/code&gt; and &lt;code&gt;make&lt;/code&gt; commands installed.&lt;/p&gt;

&lt;p&gt;Consider the &lt;a href="https://github.com/drakulavich/realworld/blob/main/Makefile" rel="noopener noreferrer"&gt;Makefile&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# Makefile
&lt;/span&gt;&lt;span class="nv"&gt;PACTICIPANT&lt;/span&gt; &lt;span class="o"&gt;?=&lt;/span&gt; &lt;span class="s2"&gt;"realworld-openapi-spec"&lt;/span&gt;

&lt;span class="c"&gt;## ====================
## Pactflow Provider Publishing
## ====================
&lt;/span&gt;&lt;span class="nv"&gt;PACT_CLI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"docker run --rm -v &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="s2"&gt;PWD&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:/app -w "&lt;/span&gt;/app&lt;span class="s2"&gt;" -e PACT_BROKER_BASE_URL -e PACT_BROKER_TOKEN pactfoundation/pact-cli"&lt;/span&gt;
&lt;span class="nv"&gt;OAS_FILE_PATH&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;api/openapi.yml
&lt;span class="nv"&gt;OAS_FILE_CONTENT_TYPE&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;application/yaml
&lt;span class="nv"&gt;REPORT_FILE_PATH&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;api/README.md
&lt;span class="nv"&gt;REPORT_FILE_CONTENT_TYPE&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;text/markdown
&lt;span class="nv"&gt;VERIFIER_TOOL&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;newman

&lt;span class="c"&gt;# Export all variable to sub-make if .env exists
&lt;/span&gt;&lt;span class="k"&gt;ifneq&lt;/span&gt; &lt;span class="nv"&gt;(,$(wildcard ./.env))&lt;/span&gt;
    &lt;span class="k"&gt;include&lt;/span&gt;&lt;span class="sx"&gt; .env&lt;/span&gt;
    &lt;span class="err"&gt;export&lt;/span&gt;
&lt;span class="k"&gt;endif&lt;/span&gt;

&lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;cat&lt;/span&gt; ./Makefile

&lt;span class="nl"&gt;ci&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;ci-test publish_pacts can_i_deploy&lt;/span&gt;

&lt;span class="c"&gt;# Run the ci target from a developer machine with the environment variables
# set as if it was on CI.
# Use this for quick feedback when playing around with your workflows.
&lt;/span&gt;&lt;span class="nl"&gt;fake_ci&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nv"&gt;CI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;GIT_COMMIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--short&lt;/span&gt; HEAD&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;GIT_BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--abbrev-ref&lt;/span&gt; HEAD&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    make ci

&lt;span class="c"&gt;## =====================
## Build/test tasks
## =====================
&lt;/span&gt;
&lt;span class="nl"&gt;ci-test&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;========== STAGE: CI Tests ==========&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;## =====================
## Pact tasks
## =====================
&lt;/span&gt;
&lt;span class="nl"&gt;publish_pacts&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;========== STAGE: publish provider contract (spec + results) - success ==========&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;PACTICIPANT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;PACTICIPANT&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="s2"&gt;PACT_CLI&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; pactflow publish-provider-contract &lt;span class="se"&gt;\&lt;/span&gt;
    /app/&lt;span class="p"&gt;${&lt;/span&gt;OAS_FILE_PATH&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--provider&lt;/span&gt; &lt;span class="p"&gt;${&lt;/span&gt;PACTICIPANT&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--provider-app-version&lt;/span&gt; &lt;span class="p"&gt;${&lt;/span&gt;GIT_COMMIT&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--branch&lt;/span&gt; &lt;span class="p"&gt;${&lt;/span&gt;GIT_BRANCH&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--content-type&lt;/span&gt; &lt;span class="p"&gt;${&lt;/span&gt;OAS_FILE_CONTENT_TYPE&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--verification-exit-code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--verification-results&lt;/span&gt; /app/&lt;span class="p"&gt;${&lt;/span&gt;REPORT_FILE_PATH&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--verification-results-content-type&lt;/span&gt; &lt;span class="p"&gt;${&lt;/span&gt;REPORT_FILE_CONTENT_TYPE&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--verifier&lt;/span&gt; &lt;span class="p"&gt;${&lt;/span&gt;VERIFIER_TOOL&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nl"&gt;deploy&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;deploy_app record_deployment&lt;/span&gt;

&lt;span class="nl"&gt;can_i_deploy&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;========== STAGE: can-i-deploy? 🌉 ==========&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="s2"&gt;PACT_CLI&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; broker can-i-deploy &lt;span class="nt"&gt;--pacticipant&lt;/span&gt; &lt;span class="p"&gt;${&lt;/span&gt;PACTICIPANT&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt; &lt;span class="p"&gt;${&lt;/span&gt;GIT_COMMIT&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--to-environment&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="nl"&gt;deploy_app&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;========== STAGE: deploy ==========&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Deploying to test"&lt;/span&gt;

&lt;span class="nl"&gt;record_deployment&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="s2"&gt;PACT_CLI&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; broker record-deployment &lt;span class="nt"&gt;--pacticipant&lt;/span&gt; &lt;span class="p"&gt;${&lt;/span&gt;PACTICIPANT&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt; &lt;span class="p"&gt;${&lt;/span&gt;GIT_COMMIT&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--environment&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c"&gt;## =====================
## Misc
## =====================
&lt;/span&gt;
&lt;span class="nl"&gt;.env&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; .env.example .env &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;make .env&lt;/code&gt; to create &lt;code&gt;.env&lt;/code&gt; file. You will need to update two variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PACT_BROKER_BASE_URL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PACT_BROKER_TOKEN&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;[Optional]&lt;/em&gt; We may try to publish the contract from the local machine using &lt;code&gt;make fake-ci&lt;/code&gt;. This target under the hood executes the following stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ci-test&lt;/code&gt; runs functional tests (skipped in our case)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;publish_pacts&lt;/code&gt; uses pactflow docker image to publish current OAS to Pactflow broker. &lt;code&gt;GIT_COMMIT&lt;/code&gt; and &lt;code&gt;GIT_BRANCH&lt;/code&gt; environment variables are used to set the version tag and the branch on the broker side.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;can_i_deploy&lt;/code&gt; verifies that the published contract is compatible with dependent consumers. Hence we are safe to deploy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the last piece. Let's create a &lt;a href="https://github.com/drakulavich/realworld/blob/main/.github/workflows/pact.yml" rel="noopener noreferrer"&gt;GitHub workflow&lt;/a&gt; to publish OAS on each push to the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/pact.yml&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pact CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;paths-ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*.md'&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# For pull requests, cancel all currently-running jobs for this workflow&lt;/span&gt;
  &lt;span class="c1"&gt;# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workflow }}-${{ github.head_ref || github.run_id }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;PACT_BROKER_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://drakulavich.pactflow.io&lt;/span&gt;
  &lt;span class="na"&gt;PACT_BROKER_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PACT_BROKER_TOKEN }}&lt;/span&gt;
  &lt;span class="na"&gt;GIT_COMMIT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.sha }}&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Inject slug/short variables&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rlespinasse/github-slug-action@v4&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GIT_BRANCH=${GITHUB_REF_NAME_SLUG} make ci&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;verify&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.ref == 'refs/heads/main' }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Inject slug/short variables&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rlespinasse/github-slug-action@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🚀 Record deployment on Pactflow&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GIT_BRANCH=${GITHUB_REF_NAME_SLUG} make deploy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub will use the same &lt;code&gt;Makefile&lt;/code&gt;. Please, have a look at the Deploy job from the workflow above. We are executing &lt;code&gt;make deploy&lt;/code&gt; to explicitly mark the deployed version for the test environment on the Pactflow broker.&lt;/p&gt;

&lt;p&gt;We will provide the environment variable to access Pactflow differently. Don't forget to add the secret &lt;code&gt;PACT_BROKER_TOKEN&lt;/code&gt; in Settings — Secrets — New repository secret.&lt;/p&gt;

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

&lt;p&gt;Ok. We are ready to commit the files! If everything is fine, the pipeline should be green.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftqh83q2tgkfahe63aaw0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftqh83q2tgkfahe63aaw0.png" alt="Pipeline to publish OAS to Pactflow" width="800" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open your Pactflow workspace and check out the Provider Contract. You should be able to open Open API Swagger spec.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh4bgx8kest8tqk1bq9w1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh4bgx8kest8tqk1bq9w1.png" alt="Open API Swagger on Pactflow" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, we finished with the first part. We have CI/CD for Provider Contract. Let's move on to the second part.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Consumer contract
&lt;/h2&gt;

&lt;p&gt;We will use &lt;a href="https://github.com/angelguzmaning/ts-redux-react-realworld-example-app" rel="noopener noreferrer"&gt;ts-redux-react-realworld-example-app&lt;/a&gt; project which implements the front-end in TS for realword app using React/Redux.&lt;/p&gt;

&lt;p&gt;If you want to follow the same approach, I suggest you to check &lt;a href="https://github.com/pact-foundation/pact-workshop-js" rel="noopener noreferrer"&gt;&lt;strong&gt;pact-workshop-js&lt;/strong&gt;&lt;/a&gt; for better understanding.&lt;/p&gt;

&lt;p&gt;1) We'll start by installing &lt;code&gt;@pact-foundation/pact&lt;/code&gt; dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @pact-foundation/pact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Create directory &lt;code&gt;src/services/consumer/&lt;/code&gt;&lt;br&gt;
3) Create &lt;code&gt;src/services/consumer/apiPactProvider.ts&lt;/code&gt;&lt;br&gt;
Here we'll describe the pact specification format and name of our &lt;strong&gt;pact&lt;/strong&gt;icipants (participants).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/consumer/apiPactProvider.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PactV3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MatchersV3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SpecificationVersion&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pact-foundation/pact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;eachLike&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;like&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MatchersV3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PactV3&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ts-redux-react-realworld-example-app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;realworld-openapi-spec&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pacts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SpecificationVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SPECIFICATION_VERSION_V2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4) Create &lt;code&gt;src/services/consumer/api.pact.spec.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Let's start with something tiny and achievable. We would like to check &lt;code&gt;/tags&lt;/code&gt; endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/consumer/api.pact.spec.ts&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API Pact tests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getting all tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tags exist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// set up Pact interactions&lt;/span&gt;

      &lt;span class="c1"&gt;// Execute provider interaction&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From &lt;a href="https://github.com/pact-foundation/pact-workshop-js" rel="noopener noreferrer"&gt;&lt;strong&gt;pact-workshop-js&lt;/strong&gt;&lt;/a&gt; you will learn that each Pact test contains two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Setup Pact interactions.&lt;/li&gt;
&lt;li&gt;Execute the interaction using the API client.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first part might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/consumer/api.pact.spec.ts&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tagsResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reactjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;angularjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="c1"&gt;// set up Pact interactions&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInteraction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tags exist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="na"&gt;uponReceiving&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get all tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;withRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;willRespondWith&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tagsResponse&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We created a stub for the response &lt;code&gt;tagsResponse&lt;/code&gt;. Then we added interaction to the provider object from &lt;code&gt;src/services/consumer/apiPactProvider.ts&lt;/code&gt;. The essential parts relate to the expected request and how the provider will reply.&lt;/p&gt;

&lt;p&gt;In the second part we will add the test execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/consumer/api.pact.spec.ts&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mockService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// make request to Pact mock server&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getTags&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toStrictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tagsResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ultimately we need to change the URL for the API client and execute the request after it. In our case we have to alter axios &lt;code&gt;baseURL&lt;/code&gt;, because all API interactions explicitly use axios. For example, &lt;code&gt;getTags()&lt;/code&gt; implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getTags&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}))((&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We want to verify that response is the same as a prepared stub. After collecting all parts inside the test file we will get the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/consumer/api.pact.spec.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;like&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./apiPactProvider&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getTags&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../conduit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API Pact tests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getting all tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tags exist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tagsResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reactjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;angularjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="c1"&gt;// set up Pact interactions&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInteraction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tags exist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="na"&gt;uponReceiving&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get all tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;withRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;willRespondWith&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tagsResponse&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mockService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// make request to Pact mock server&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getTags&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toStrictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tagsResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's run &lt;code&gt;make fake-ci&lt;/code&gt; to check how is going:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Computer says &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="se"&gt;\o&lt;/span&gt;/ 

CONSUMER                             | C.VERSION          | PROVIDER               | P.VERSION  | SUCCESS? | RESULT#
&lt;span class="nt"&gt;-------------------------------------&lt;/span&gt;|--------------------|------------------------|------------|----------|--------
ts-redux-react-realworld-example-app | d981433+1663657371 | realworld-openapi-spec | 95dbd23... | &lt;span class="nb"&gt;true&lt;/span&gt;     | 1      
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hooray! It's a huge step towards contract testing.&lt;br&gt;
Now let's try to test another endpoint &lt;code&gt;GET /user&lt;/code&gt;. We will start with the same ideas: create a stub, prepare interactions and execute API client call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/consumer/api.pact.spec.ts&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getting current user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user exists&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;tUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jake@jake.jake&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jwt.token.here&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jake&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;I work at statefarm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://i.stack.imgur.com/xHWG8.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="c1"&gt;// set up Pact interactions&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInteraction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user has logged in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="na"&gt;uponReceiving&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;withRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;willRespondWith&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userResponse&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mockService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// make request to Pact mock server&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toStrictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;If you run &lt;code&gt;make fake-ci&lt;/code&gt;, you will get an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Computer says no ¯_&lt;span class="o"&gt;(&lt;/span&gt;ツ&lt;span class="o"&gt;)&lt;/span&gt;_/¯
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the generated report link and you will see the details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request Authorization header is missing but is required by the spec file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;We missed Authorization header that is required to check logged in user. Let's add the stub (1), headers to the expected request (2) and axios defaults (3):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/consumer/api.pact.spec.ts&lt;/span&gt;

      &lt;span class="c1"&gt;// (1)&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Token xxxxxx.yyyyyyy.zzzzzz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

        &lt;span class="nl"&gt;uponReceiving&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;withRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nl"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// (2)&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mockService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Authorization&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// (3)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After next &lt;code&gt;make fake-ci&lt;/code&gt; attempt you will get the new error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;FAIL src/services/consumer/api.pact.spec.ts
  ● Console

    console.error
      Error: Cross origin http://localhost forbidden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I found the fix for it &lt;a href="https://stackoverflow.com/a/67880430/4894316" rel="noopener noreferrer"&gt;on StackOverflow&lt;/a&gt;. We need to add the following line on the top of the test file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;axios.defaults.adapter = require('axios/lib/adapters/http');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next &lt;code&gt;make fake-ci&lt;/code&gt; should be successful.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We have learned how to configure Provider and Consumer for bi-directional contract testing. We used RealWorld project to practice integration with Pactflow broker. And finally, we wrote a couple of consumer tests for ts-redux-react-realworld-example-app. We also learned how to debug the tests using &lt;code&gt;make fake-ci&lt;/code&gt; command. All of the examples are available on GitHub. Feel free to try and post your questions if you face the issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Materials
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/drakulavich/realworld" rel="noopener noreferrer"&gt;RealWorld repo&lt;/a&gt; with Open API spec (provider contract)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/drakulavich/ts-redux-react-realworld-example-app" rel="noopener noreferrer"&gt;ts-redux-react-realworld-example-app&lt;/a&gt; with pact tests (consumer)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pact-foundation/pact-workshop-js" rel="noopener noreferrer"&gt;pact js workshop&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>testing</category>
      <category>devops</category>
      <category>pactflow</category>
    </item>
    <item>
      <title>GitLab CI: Cache and Artifacts explained by example</title>
      <dc:creator>Anton Yakutovich</dc:creator>
      <pubDate>Wed, 04 Aug 2021 10:13:46 +0000</pubDate>
      <link>https://dev.to/drakulavich/gitlab-ci-cache-and-artifacts-explained-by-example-2opi</link>
      <guid>https://dev.to/drakulavich/gitlab-ci-cache-and-artifacts-explained-by-example-2opi</guid>
      <description>&lt;p&gt;Hi, DEV Community! I've been working in the software testing field for more than eight years. Apart from web services testing, I maintain CI/CD Pipelines in our team's GitLab.&lt;/p&gt;

&lt;p&gt;Let's discuss the difference between GitLab cache and artifacts. I'll show how to configure the Pipeline for the Node.js app in a pragmatic way to achieve good performance and resource utilization.&lt;/p&gt;

&lt;p&gt;There are three things you can watch forever: fire burning, water falling, and the build is passing after your next commit. Nobody wants to wait for the CI completion too much, it's better to set up all the tweaks to avoid long waiting between the commit the build status. Cache and artifacts to the rescue! They help reduce the time it takes to run a Pipeline drastically.&lt;/p&gt;

&lt;p&gt;People are confused when they have to choose between cache and artifacts. GitLab has bright documentation, but &lt;a href="https://docs.gitlab.com/ee/ci/caching/#cache-nodejs-dependencies" rel="noopener noreferrer"&gt;the Node.js app with cache example&lt;/a&gt; and the Pipeline &lt;a href="https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml" rel="noopener noreferrer"&gt;template for Node.js&lt;/a&gt; contradict each other.&lt;/p&gt;

&lt;p&gt;Let's see what the Pipeline in GitLab terms means. The &lt;a href="https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html" rel="noopener noreferrer"&gt;Pipeline&lt;/a&gt; is a set of stages and each stage can have one or more jobs. Jobs work on a distributed farm of runners. When we start a Pipeline, a random runner with free resources executes the needed job. The GitLab-runner is the agent that can run jobs. For simplicity, let's consider Docker as an executor for all runners.&lt;/p&gt;

&lt;p&gt;Each job starts with a clean slate and doesn't know the results of the previous one. If you don't use cache and artifacts, the runner will have to go to the internet or local registry and download the necessary packages when installing project dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is cache?
&lt;/h3&gt;

&lt;p&gt;It's a set of files that a job can download before running and upload after execution. By default, the cache is stored in the same place where GitLab Runner is installed. If the distributed cache is configured, S3 works as storage.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faz3xa3hssyj8q12s6fiw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faz3xa3hssyj8q12s6fiw.png" alt="GitLab Cache" width="800" height="200"&gt;&lt;/a&gt;&lt;br&gt;
Let's suppose you run a Pipeline for the first time with a local cache. The job will not find the cache but will upload one after the execution to runner01. The second job will execute on runner02, it won't find the cache on it either and will work without it. The result will be saved to runner02. Lint, the third job, will find the cache on runner01 and use it (pull). After execution, it will upload the cache back (push).&lt;/p&gt;
&lt;h3&gt;
  
  
  What are artifacts?
&lt;/h3&gt;

&lt;p&gt;Artifacts are files stored on the GitLab server after a job is executed. Subsequent jobs will download the artifact before script execution.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpappaj15pbvty5z7k38q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpappaj15pbvty5z7k38q.png" alt="GitLab artifacts" width="799" height="373"&gt;&lt;/a&gt;&lt;br&gt;
Build job creates a DEF artifact and saves it on the server. The second job, Test, downloads the artifact from the server before running the commands. The third job, Lint, similarly downloads the artifact from the server. &lt;/p&gt;

&lt;p&gt;To compare the artifact is created in the first job and is used in the following ones. The cache is created within each job.&lt;/p&gt;

&lt;p&gt;Consider the CI template example for Node.js recommended by GitLab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;image: node:latest # (1)

# This folder is cached between builds
cache:
  paths:
    - node_modules/ # (2)

test_async:
  script:
    - npm install # (3)
    - node ./specs/start.js ./specs/async.spec.js

test_db:
  script:
    - npm install # (4)
    - node ./specs/start.js ./specs/db-postgres.spec.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line #1 specifies the docker image, which will be used in all jobs. The first problem is the &lt;code&gt;latest&lt;/code&gt; tag. This tag ruins the reproducibility of the builds. It always points to the latest release of Node.js. If the GitLab runner caches docker images, the first run will download the image, and all subsequent runs will use the locally available image. So, even if a node is upgraded from version XX to YY, our Pipeline will know nothing about it. Therefore, I suggest specifying the version of the image. And not just the release branch (&lt;code&gt;node:14&lt;/code&gt;), but the full version tag (&lt;code&gt;node:14.2.5&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Line #2 is related to lines 3 and 4. The &lt;code&gt;node_modules&lt;/code&gt; directory is specified for caching, the installation of packages (npm install) is performed for every job. The installation should be faster because packages are available inside &lt;code&gt;node_modules&lt;/code&gt;. Since no key is specified for the cache, the word &lt;code&gt;default&lt;/code&gt; will be used as a key. It means that the cache will be permanent, shared between all git branches.&lt;/p&gt;

&lt;p&gt;Let me remind you, the main goal is to keep the pipeline &lt;em&gt;reproducible&lt;/em&gt;. &lt;strong&gt;The Pipeline launched today should work the same way in a year&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NPM stores dependencies in two files — &lt;em&gt;package.json&lt;/em&gt; and &lt;em&gt;package-lock.json&lt;/em&gt;. If you use &lt;em&gt;package.json&lt;/em&gt;, the build is not reproducible. When you run &lt;code&gt;npm install&lt;/code&gt; the package manager puts the last minor release for not strict dependencies. To fix the dependency tree, we use the &lt;em&gt;package-lock.json&lt;/em&gt; file. All versions of packages are strictly specified there.&lt;/p&gt;

&lt;p&gt;But there is another problem, &lt;code&gt;npm install&lt;/code&gt; rewrites package-lock.json, and this is not what we expect. Therefore, we use the special command &lt;code&gt;npm ci&lt;/code&gt; which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;removes the node_modules directory;&lt;/li&gt;
&lt;li&gt;installs packages from package-lock.json.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What shall we do if &lt;code&gt;node_modules&lt;/code&gt; will be deleted every time? We can specify NPM cache using the environment variable &lt;code&gt;npm_config_cache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And the last thing, the config does not explicitly specify the stage where jobs are executed. By default, the job runs inside the test stage. It turns out that both jobs will run in parallel. Perfect! Let's add jobs stages and fix all the issues we found.&lt;/p&gt;

&lt;p&gt;What we got after the first iteration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;image: node: 16.3.0 # (1)

stages:
  - test

variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm" (5)

# This folder is cached between builds
cache:
  key:
    files:
      - package-lock.json (6)
  paths:
    - .npm # (2)

test_async:
  stage: test
  script:
    - npm ci # (3)
    - node ./specs/start.js ./specs/async.spec.js

test_db:
  stage: test
  script:
    - npm ci # (4)
    - node ./specs/start.js ./specs/db-postgres.spec.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We improved Pipeline and make it reproducible. There are two drawbacks left. First, the cache is shared. Every job will pull the cache and push the new version after executing the job. It's a good practice to update cache only once inside Pipeline. Second, every job installs the package dependencies and wastes time.&lt;/p&gt;

&lt;p&gt;To fix the first problem we describe the cache management explicitly. Let's add a "hidden" job and enable only pull policy (download cache without updating):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Define a hidden job to be used with extends
# Better than default to avoid activating cache for all jobs
.dependencies_cache:
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - .npm
    policy: pull
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To connect the cache you need to inherit the job via &lt;code&gt;extends&lt;/code&gt; keyword.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
extends: .dependencies_cache
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To fix the second issue we use artifacts. Let's create the job that archives package dependencies and passes the artifact with &lt;code&gt;node_modules&lt;/code&gt; further. Subsequent jobs will run tests from the spot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;setup:
  stage: setup
  script:
    - npm ci
  extends: .dependencies_cache
  cache:
    policy: pull-push
  artifacts:
    expire_in: 1h
    paths:
      - node_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We install the npm dependencies and use the cache described in the hidden dependencies_cache job. Then we specify how to update the cache via a pull-push policy. A short lifetime (1 hour) helps to save space for the artifacts. There is no need to keep &lt;code&gt;node_modules&lt;/code&gt; artifact for a long time on the GitLab server.&lt;/p&gt;

&lt;p&gt;The full config after the changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;image: node: 16.3.0 # (1)

stages:
  - setup
  - test

variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm" (5)

# Define a hidden job to be used with extends
# Better than default to avoid activating cache for all jobs
.dependencies_cache:
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - .npm
    policy: pull

setup:
  stage: setup
  script:
    - npm ci
  extends: .dependencies_cache
  cache:
    policy: pull-push
  artifacts:
    expire_in: 1h
    paths:
      - node_modules

test_async:
  stage: test
  script:
    - node ./specs/start.js ./specs/async.spec.js

test_db:
  stage: test
  script:
    - node ./specs/start.js ./specs/db-postgres.spec.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We learned what's the difference between cache and artifacts. We built a reproducible Pipeline that works predictably and uses resources efficiently. This article shows some common mistakes and how to avoid them when you are setting up CI in GitLab.&lt;br&gt;
I wish you green builds and fast pipelines. Would appreciate your feedback in the comments!&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html" rel="noopener noreferrer"&gt;Pipeline architecture&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.gitlab.com/ee/ci/caching/" rel="noopener noreferrer"&gt;Caching in GitLab CI/CD&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>node</category>
      <category>cicd</category>
      <category>gitlab</category>
    </item>
  </channel>
</rss>
