<?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: Denis Morozov</title>
    <description>The latest articles on DEV Community by Denis Morozov (@frozer).</description>
    <link>https://dev.to/frozer</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F224168%2F14246663-e58d-4d61-a308-8513cf9cb50b.png</url>
      <title>DEV Community: Denis Morozov</title>
      <link>https://dev.to/frozer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/frozer"/>
    <language>en</language>
    <item>
      <title>Your Retry Budget Is Not a Safety Net</title>
      <dc:creator>Denis Morozov</dc:creator>
      <pubDate>Tue, 18 Aug 2026 21:40:12 +0000</pubDate>
      <link>https://dev.to/frozer/your-retry-budget-is-not-a-safety-net-o1k</link>
      <guid>https://dev.to/frozer/your-retry-budget-is-not-a-safety-net-o1k</guid>
      <description>&lt;h2&gt;
  
  
  *Second in a series on The Factory. Previously: &lt;a href="https://dev.to/frozer/the-factory-that-merged-37-tasks-what-happens-when-the-harness-stops-believing-the-agent-5578"&gt;The Factory That Merged 37 Tasks&lt;/a&gt;. The harness is at &lt;a href="https://github.com/frozer/factory" rel="noopener noreferrer"&gt;github.com/frozer/factory&lt;/a&gt;.
&lt;/h2&gt;

&lt;p&gt;The public description of my task harness ends on a claim: &lt;em&gt;a packet that's wrong about the world fails identically on every retry.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That sentence cost me four dead tasks and nine commits spent repairing task definitions instead of writing code. It reads like something you'd arrive at by thinking. I arrived at it by watching the same failure scroll past three times in a row.&lt;/p&gt;

&lt;h2&gt;
  
  
  What three attempts is actually good for
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;max_attempts = 3&lt;/code&gt; felt like obvious hygiene. Models are stochastic. Sometimes a run goes sideways for no reason you can name — a bad turn, a truncated response, a tool call that gets refused. Retry it and it works. That's real, and a retry budget handles it well.&lt;/p&gt;

&lt;p&gt;The strength is exactly the constraint. A retry budget assumes &lt;strong&gt;the next attempt will differ from the last one&lt;/strong&gt;. It buys you a second sample from a distribution.&lt;/p&gt;

&lt;p&gt;But a retry doesn't hand the model a fresh situation. It hands it &lt;em&gt;the same packet back&lt;/em&gt;. Same file, same claims, same instructions. If the packet says a file lives at a path where no file lives, attempt three fails precisely where attempt one did, and the only thing three attempts bought was three times the bill.&lt;/p&gt;

&lt;p&gt;Failure without variance isn't flakiness. It's a specification defect wearing a reliability costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody had opened the files
&lt;/h2&gt;

&lt;p&gt;Here's what that looked like in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B03&lt;/strong&gt; was a loader for a national census dataset. Three attempts, all burned, all against a file shape that existed nowhere: wrong directory, wrong filenames, and a &lt;code&gt;Data&lt;/code&gt; / &lt;code&gt;Valor&lt;/code&gt; JSON envelope that appears nowhere in the actual data tree. Every attempt produced a parser for a document that doesn't exist.&lt;/p&gt;

&lt;p&gt;The packet was the defect, not the model. Nobody had opened the actual files before cutting it.&lt;/p&gt;

&lt;p&gt;Rewritten from the real JSON, the truth was a flat &lt;code&gt;metadata&lt;/code&gt;/&lt;code&gt;data&lt;/code&gt; envelope, four files — one of which shouldn't be loaded at all — and a long-format pivot with genuine missing rows. Nothing about that is inferable from a design document. It's only knowable by opening the file.&lt;/p&gt;

&lt;p&gt;The same check applied to two sibling packets turned up a schema mismatch, a missed pivot, and a real design collision: two source files sharing five of six category labels &lt;em&gt;verbatim&lt;/em&gt;, with no column to tell them apart. Loading both, as the packet instructed, would have silently overwritten data. Also, the packet's confident claim about Spanish decimal commas was simply false for that data — zero commas in any total column, across all three files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A11&lt;/strong&gt; is the cleaner case, because it removes the model from the argument entirely. Its invariant required two fields — a source SRID and an attribution string — routed through a typed config. Those fields did not exist on the type. And the file that defines the type was outside A11's editable set.&lt;/p&gt;

&lt;p&gt;So no diff the implementer was permitted to produce could satisfy that invariant. Not a hard task. An impossible one, three times, at full price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C09&lt;/strong&gt; was the expensive kind. Its steps read &lt;code&gt;build → validate → write&lt;/code&gt;, which is a sensible order and reads perfectly. But the &lt;code&gt;build&lt;/code&gt; call persisted internally before returning — so build had already written. All three attempts validated &lt;em&gt;after&lt;/em&gt; the write, and dutifully logged the failure that the invariant existed to prevent.&lt;/p&gt;

&lt;p&gt;Nothing in the worktree tells a model any of this. It fails, gets handed the same packet, and fails the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A retry budget is for variance, not for error.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before you raise &lt;code&gt;max_attempts&lt;/code&gt;, read the three failures side by side and ask one question: &lt;em&gt;did they differ?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If attempt three failed differently from attempt one, retries are doing their job. If all three failed identically, you are not looking at an unreliable model. You are looking at an instruction that cannot be followed, and you are paying full price to have that confirmed repeatedly.&lt;/p&gt;

&lt;p&gt;The retry loop is the last place to fix this and the first place everyone looks.&lt;/p&gt;

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

&lt;p&gt;The fix wasn't a better implementer prompt. It was admitting that the cheapest place to catch a defective packet is before any model has cost anything.&lt;/p&gt;

&lt;p&gt;So the planning step got its own two agents. One cuts packets from the spec. A second one — with no ability to edit anything, because a run that can edit what it's judging will eventually repair a defect quietly instead of reporting it — assesses each packet and rules on every checkable claim in it: &lt;code&gt;held&lt;/code&gt;, &lt;code&gt;violated&lt;/code&gt;, or &lt;code&gt;unverifiable&lt;/code&gt;, each with a &lt;code&gt;path:line&lt;/code&gt; or a command's output. The queue now refuses any packet without a passing assessment pinned to that packet's exact hash.&lt;/p&gt;

&lt;p&gt;The cutter works from a ground-truth ladder: files on disk first, then the code the task will call, then what earlier packets produce, then the toolchain — and the spec ranks &lt;em&gt;below all of them&lt;/em&gt;, as evidence of intent rather than of fact.&lt;/p&gt;

&lt;p&gt;That ordering is the whole lesson. A spec tells you what someone wanted. Only the file tells you what's there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I didn't expect
&lt;/h2&gt;

&lt;p&gt;I backtested the assessor against those nine repair commits as a labelled set. All five known-bad packets came back &lt;code&gt;recut&lt;/code&gt;, each naming the same defect the human fix had found. The two I'd already repaired came back &lt;code&gt;ready&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three packets that had &lt;em&gt;shipped&lt;/em&gt; came back &lt;code&gt;recut&lt;/code&gt; — for true reasons.&lt;/p&gt;

&lt;p&gt;And assessing A11 surfaced a live bug in merged code: a loader calling a &lt;code&gt;snapshot_id&lt;/code&gt; property on a directory path, where the property reads a single file's bytes. That code had passed the linter, passed the tests, passed review, and merged.&lt;/p&gt;

&lt;p&gt;No test could have caught it. The dataset isn't in this checkout, so the offline suite never reaches the call.&lt;/p&gt;

&lt;p&gt;A green test suite told me that code was fine. A model reading the packet next to the code found it in one pass.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>agents</category>
      <category>thefactory</category>
    </item>
    <item>
      <title>The Factory That Merged 37 Tasks: What Happens When the Harness Stops Believing the Agent</title>
      <dc:creator>Denis Morozov</dc:creator>
      <pubDate>Wed, 12 Aug 2026 12:17:13 +0000</pubDate>
      <link>https://dev.to/frozer/the-factory-that-merged-37-tasks-what-happens-when-the-harness-stops-believing-the-agent-5578</link>
      <guid>https://dev.to/frozer/the-factory-that-merged-37-tasks-what-happens-when-the-harness-stops-believing-the-agent-5578</guid>
      <description>&lt;p&gt;Back in March I wrote that in an AI-native company, processes stop being folklore and become measurable artifacts — versioned, evaluated, improvable. That was a claim. This is the receipt.&lt;/p&gt;

&lt;p&gt;Seven days in August. A harness I built drained a queue of 37 task packets. Each one implemented by a model in its own git worktree, verified by tests the harness ran itself, reviewed against invariants the packet declared by name, squash-merged onto the feature branch only if it survived all three.&lt;/p&gt;

&lt;p&gt;172 files changed. 29,360 lines added. The test suite it was building went from &lt;strong&gt;81 passing tests to 422&lt;/strong&gt;. Direct cost: $3.89.&lt;/p&gt;

&lt;p&gt;Thirty-seven packets, thirty-seven merges, and I did not read a single diff before it landed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this came from
&lt;/h2&gt;

&lt;p&gt;This didn't start as an idea. It started as a habit I'd been demoing to my team for the better part of a year.&lt;/p&gt;

&lt;p&gt;The workflow was manual and it was simple: plan first, read the plan yourself, ask for the implementation, then open a &lt;strong&gt;fresh&lt;/strong&gt; model with no memory of writing the code and ask it to assess what changed. Separately, &lt;code&gt;git worktree&lt;/code&gt; so several agents could work different changes in the same project without stepping on each other. Nothing clever. It just worked better than letting one long conversation both write the code and reassure me about it.&lt;/p&gt;

&lt;p&gt;I ran that by hand for months. Then the honest question: if the loop is always plan → implement → assess-with-a-clean-model, and I'm the one clicking through it forty times, what exactly am I contributing?&lt;/p&gt;

&lt;p&gt;The answer was &lt;code&gt;git worktree&lt;/code&gt;, a SQLite file, and about a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a packet is
&lt;/h2&gt;

&lt;p&gt;The unit of work isn't a prompt. It's a file in git.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="err"&gt;+++&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"B01"&lt;/span&gt;
&lt;span class="py"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"adrh-loader"&lt;/span&gt;
&lt;span class="py"&gt;goal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"add ingest/adrh_load.py — the ADRH income CSV, all levels, all years"&lt;/span&gt;
&lt;span class="py"&gt;tier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"standard"&lt;/span&gt;
&lt;span class="py"&gt;gate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"auto"&lt;/span&gt;
&lt;span class="py"&gt;surface&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"api"&lt;/span&gt;
&lt;span class="py"&gt;spec_commit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"504abb9dca9e050fda5e85a665f5bd2b66c2944b"&lt;/span&gt;
&lt;span class="py"&gt;spec_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"docs/IMPLEMENTATION-S3-reporting-baseline.md"&lt;/span&gt;
&lt;span class="py"&gt;requires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"A08"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"A10"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;max_attempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;forbidden_paths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"api/ingest/vintage.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"api/tests/conftest.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[[invariants]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"thousands-separator"&lt;/span&gt;
&lt;span class="py"&gt;critical&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"`16.429` loads as 16429, not 16.429. The dot is a thousands separator."&lt;/span&gt;

&lt;span class="nn"&gt;[[invariants]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"empty-total-loads-as-null"&lt;/span&gt;
&lt;span class="py"&gt;critical&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"An empty `Total` cell loads as NULL, never 0 and never a skipped row. 15,480 rows in the source have one."&lt;/span&gt;
&lt;span class="err"&gt;+++&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two fields do most of the work.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spec_commit&lt;/code&gt; pins the packet to the exact revision of the design document it was cut from. If the spec moves, the packet is stale, and the harness refuses to run it rather than building something the design stopped asking for.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[[invariants]]&lt;/code&gt; matters more. Each one has an &lt;strong&gt;id&lt;/strong&gt;, and the reviewer must return a verdict on every id — &lt;code&gt;held&lt;/code&gt;, &lt;code&gt;violated&lt;/code&gt;, or &lt;code&gt;unverifiable&lt;/code&gt; — with a &lt;code&gt;file:line&lt;/code&gt; behind it. A review that quietly skips one is rejected by the contract. Silence is not a pass.&lt;/p&gt;

&lt;p&gt;Now look at what those two invariants actually say. Not "parse the CSV correctly." A specific dot in a specific number. A count — 15,480 rows — that somebody went and measured in the real file.&lt;/p&gt;

&lt;p&gt;That precision isn't style. It turned out to be the entire difference between packets that worked and packets that burned every attempt they had. More on that next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ready ──claim──▶ running ──▶ gating ──green──▶ reviewing ──accept──▶ merging ──▶ done
  ▲                            │                    │                    │
  └────── needs_work ◀─────────┴── red ──────┴── revise ──┘         awaiting_human
                 │
                 └── attempts exhausted ──▶ blocked ──▶ the loop halts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per task: claim it, check the spec hasn't moved, create a worktree, measure the test baseline, plant the supplied test files and commit them, implement, gate, review, merge.&lt;/p&gt;

&lt;p&gt;Two exits are not &lt;code&gt;done&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;blocked&lt;/code&gt; &lt;strong&gt;halts the whole queue&lt;/strong&gt; instead of skipping ahead. A task that failed three times has usually revealed something wrong with its packet, and running the next twenty on the same misunderstanding wastes more than it saves.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;awaiting_human&lt;/code&gt; is where a packet goes when I marked it &lt;code&gt;gate = "human"&lt;/code&gt; at cut time. Five of the 37 carried that flag, on one rule: the failure is &lt;strong&gt;silent&lt;/strong&gt; &lt;em&gt;and&lt;/em&gt; the blast radius is the whole stage. A migration that would need undoing on live data. A cache that never hits — every assertion still passes, only latency changes. A chart that draws backwards while the numbers printed next to it stay correct.&lt;/p&gt;

&lt;p&gt;An automated green gate cannot certify an invariant that fails green. Pretending otherwise just merges it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one idea
&lt;/h2&gt;

&lt;p&gt;Everything above is mechanism. The actual claim is one sentence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The harness never believes the agent.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The implementer returns a &lt;code&gt;verification&lt;/code&gt; block saying its tests pass. That block is recorded and ignored. &lt;code&gt;ruff check&lt;/code&gt;, &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;npm run lint&lt;/code&gt; and &lt;code&gt;npm run build&lt;/code&gt; are run by the harness, in the worktree, and &lt;em&gt;its&lt;/em&gt; parse of the output decides whether the work is even eligible for review.&lt;/p&gt;

&lt;p&gt;Same principle at every layer. Test files are planted into the worktree and committed &lt;strong&gt;before&lt;/strong&gt; the model starts, then declared off-limits — so &lt;code&gt;git diff&lt;/code&gt; against that commit proves the model made the test pass instead of making it agree. The reviewer's &lt;code&gt;accept&lt;/code&gt; is downgraded automatically over a red gate, a violated critical invariant, or a blocker finding, and the reason is written to a column instead of living in my head.&lt;/p&gt;

&lt;p&gt;That's the difference between letting a model write your code and having a pipeline. Not the model. Not the prompt. Refusing to accept any actor's report of its own work as evidence about that work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first real run
&lt;/h2&gt;

&lt;p&gt;Then I ran it for the first time, end to end, on one small task.&lt;/p&gt;

&lt;p&gt;The model did its job. All five invariants held, 96 tests passing. And the harness could not get the work into git.&lt;/p&gt;

&lt;p&gt;The first run turned up a cluster of bugs, every one of them mine. My favourite: the loop regenerates its status board after each merge and leaves the file modified — and the loop refuses to start on a dirty tree. It was dirtying its own working copy. It would have blocked its own next invocation.&lt;/p&gt;

&lt;p&gt;A week building a governance system for other people's agents, and the first thing it governed into a corner was itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost me
&lt;/h2&gt;

&lt;p&gt;The work didn't disappear. It moved.&lt;/p&gt;

&lt;p&gt;The harness is &lt;strong&gt;5,741 lines of stdlib-only Python&lt;/strong&gt;, plus 2,168 lines of tests for itself — 161 of them. No framework, no dependencies, runs on a bare &lt;code&gt;python3&lt;/code&gt;. That's a real week of engineering to avoid reading 37 diffs.&lt;/p&gt;

&lt;p&gt;Which is a terrible trade for 37 diffs. It's a good trade for a process that states its own failure conditions, records what every run cost, and can be pointed at the next thirty-seven.&lt;/p&gt;

&lt;p&gt;And I got the economics wrong going in. My assumption was that a cheap model does the work and a stronger one checks it, so the checking is the rounding error. The first task I measured properly: &lt;strong&gt;$0.11 to implement, $0.18 to review.&lt;/strong&gt; The review was the expensive half. I had built the whole thing on the opposite premise and found out on task one.&lt;/p&gt;

&lt;p&gt;It's public now, Apache 2.0: &lt;strong&gt;&lt;a href="https://github.com/frozer/factory" rel="noopener noreferrer"&gt;github.com/frozer/factory&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One caveat before someone finds it for me. It was built for — and has only ever run against — a repository with exactly two surfaces, an &lt;code&gt;api/&lt;/code&gt; and a &lt;code&gt;webapp/&lt;/code&gt;. Point it anywhere else and it reports an environment fault. The toolchain is narrow and the README says so.&lt;/p&gt;

&lt;p&gt;But the toolchain was never the interesting part. What generalises is the failure modes: what goes wrong when you let a model plan work for other models, and why the obvious safety net — just retry it — catches none of them.&lt;/p&gt;

&lt;p&gt;I gave every task three attempts, because three attempts felt like obvious hygiene.&lt;/p&gt;

&lt;p&gt;Four tasks in that queue burned all three. Every one of them failed identically each time.&lt;/p&gt;

&lt;p&gt;The retry budget caught nothing.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>thefactory</category>
    </item>
    <item>
      <title>The Redis That Timed Out: A Docker Subnet Overlap Hiding in Plain Sight</title>
      <dc:creator>Denis Morozov</dc:creator>
      <pubDate>Mon, 13 Jul 2026 17:58:57 +0000</pubDate>
      <link>https://dev.to/frozer/the-redis-that-timed-out-a-docker-subnet-overlap-hiding-in-plain-sight-3473</link>
      <guid>https://dev.to/frozer/the-redis-that-timed-out-a-docker-subnet-overlap-hiding-in-plain-sight-3473</guid>
      <description>&lt;p&gt;Postgres worked. Redis didn't. Same host, same Docker, same iptables, two containers started four minutes apart — and one of them refused to answer. This is the story of a &lt;code&gt;TimeoutError&lt;/code&gt; that had nothing to do with Redis, and the one-line routing fact that explained everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;My dev stack for a trading-desk API lives on an Alpine Linux VM under Windows 11 + Hyper-V. Inside Alpine, Docker runs two containers: &lt;strong&gt;Postgres&lt;/strong&gt; and &lt;strong&gt;Redis&lt;/strong&gt;, both port-published, both reachable from the host at &lt;code&gt;192.168.240.2&lt;/code&gt;. The Python app talks to Postgres happily. Then I wired up Redis for cache-aside and got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;redis.exceptions.TimeoutError: Timeout connecting to server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not &lt;em&gt;connection refused&lt;/em&gt;. &lt;strong&gt;Timeout.&lt;/strong&gt; That distinction is the whole plot.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refused&lt;/strong&gt; = the packet arrived and something said "no." Wrong port, dead service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout&lt;/strong&gt; = the packet left and &lt;em&gt;nothing ever came back&lt;/em&gt;. It's being black-holed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the SYN was going somewhere it shouldn't. But where? Redis was up (&lt;code&gt;docker exec redis redis-cli ping&lt;/code&gt; → &lt;code&gt;PONG&lt;/code&gt;), the DNAT rule looked identical to the Postgres one, and Postgres — same host, same IP — was fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clue was in &lt;code&gt;ifconfig&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Two interfaces on the Docker host, side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker0   inet addr:172.17.0.1   Mask:255.255.0.0     -&amp;gt; 172.17.0.0/16   (Redis lives here)
eth0      inet addr:172.17.6.90   Mask:255.255.240.0   -&amp;gt; 172.17.0.0/20   (Hyper-V network)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read those two subnets again. &lt;strong&gt;&lt;code&gt;docker0&lt;/code&gt; claims &lt;code&gt;172.17.0.0/16&lt;/code&gt;. The Hyper-V-assigned &lt;code&gt;eth0&lt;/code&gt; sits on &lt;code&gt;172.17.0.0/20&lt;/code&gt;.&lt;/strong&gt; They overlap. And Redis's container IP, &lt;code&gt;172.17.0.2&lt;/code&gt;, falls inside &lt;em&gt;both&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;When a client hit &lt;code&gt;192.168.240.2:6379&lt;/code&gt;, iptables DNAT'd the packet to &lt;code&gt;172.17.0.2&lt;/code&gt; — correctly. Then the kernel had to &lt;em&gt;route&lt;/em&gt; to &lt;code&gt;172.17.0.2&lt;/code&gt;, and it found two matching routes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;172.17.0.0/20&lt;/code&gt; via &lt;code&gt;eth0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;172.17.0.0/16&lt;/code&gt; via &lt;code&gt;docker0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The kernel picks the &lt;strong&gt;longest prefix&lt;/strong&gt;. &lt;code&gt;/20&lt;/code&gt; is more specific than &lt;code&gt;/16&lt;/code&gt;, so &lt;code&gt;eth0&lt;/code&gt; wins. The packet was shoved out onto the Hyper-V LAN, hunting for a &lt;code&gt;172.17.0.2&lt;/code&gt; that doesn't exist out there — instead of down &lt;code&gt;docker0&lt;/code&gt; to the container two hops away. Black hole. Timeout.&lt;/p&gt;

&lt;p&gt;One command nailed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ip route get 172.17.0.2
&lt;span class="go"&gt;172.17.0.2 dev eth0  src 172.17.6.90
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dev eth0&lt;/code&gt;. Not &lt;code&gt;docker0&lt;/code&gt;. There's your bug, printed in one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Postgres got away with it
&lt;/h2&gt;

&lt;p&gt;Here's the part that makes it feel like magic until you see it: &lt;strong&gt;Postgres never did anything special.&lt;/strong&gt; It just happened to be started with &lt;code&gt;docker compose&lt;/code&gt;, and Redis with a bare &lt;code&gt;docker run&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Compose &lt;em&gt;silently creates a dedicated user-defined bridge network per project&lt;/em&gt;, and those are allocated from Docker's address pool starting at &lt;strong&gt;&lt;code&gt;172.18.0.0/16&lt;/code&gt;&lt;/strong&gt; — the &lt;code&gt;172.17&lt;/code&gt; range is reserved for the built-in &lt;code&gt;docker0&lt;/code&gt;. So Postgres landed on &lt;code&gt;172.18.x&lt;/code&gt;, which collides with nothing. Redis, via &lt;code&gt;docker run&lt;/code&gt;, fell onto &lt;code&gt;docker0&lt;/code&gt;/&lt;code&gt;172.17&lt;/code&gt; — straight into the Hyper-V overlap.&lt;/p&gt;

&lt;p&gt;Nobody configured this. The outcome hinged entirely on &lt;code&gt;docker compose&lt;/code&gt; vs &lt;code&gt;docker run&lt;/code&gt;. It's a latent trap that only springs when your host network and Docker's defaults happen to share a prefix — and Hyper-V's Default Switch &lt;em&gt;re-rolls its subnet on almost every reboot&lt;/em&gt;, drawing from exactly the ranges Docker likes: &lt;code&gt;172.16.0.0/12&lt;/code&gt; and &lt;code&gt;192.168.0.0/16&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: evict Docker from the danger zone
&lt;/h2&gt;

&lt;p&gt;The robust move isn't to dodge one subnet — it's to relocate &lt;em&gt;all&lt;/em&gt; of Docker into the one big private block Hyper-V never touches: &lt;strong&gt;&lt;code&gt;10.0.0.0/8&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Pin Docker's pools&lt;/strong&gt; in &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.200.0.1/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default-address-pools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"base"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.201.0.0/16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bip&lt;/code&gt; moves the default &lt;code&gt;docker0&lt;/code&gt; bridge; &lt;code&gt;default-address-pools&lt;/code&gt; moves every Compose/user network. Nothing in &lt;code&gt;10.200&lt;/code&gt;/&lt;code&gt;10.201&lt;/code&gt; can ever share a prefix with a &lt;code&gt;172.x&lt;/code&gt;/&lt;code&gt;192.168.x&lt;/code&gt; Hyper-V interface — even after the next reboot re-roll.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Make it deterministic&lt;/strong&gt; by folding Redis into the same Compose file with a fixed subnet, so it stops being the odd &lt;code&gt;docker run&lt;/code&gt; container that started this mess:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opendesk&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379:6379"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opendesk&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;opendesk&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;opendesk&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
    &lt;span class="na"&gt;ipam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;subnet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.201.10.0/24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Restart and rebuild&lt;/strong&gt; (existing networks keep their old subnets until recreated):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rc-service docker restart
docker compose down &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Verify the route moved&lt;/strong&gt; — the exact test that exposed the bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip route get &lt;span class="si"&gt;$(&lt;/span&gt;docker inspect &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'&lt;/span&gt; redis&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;#  -&amp;gt; ... dev br-xxxxxxxx    (fixed)   instead of   dev eth0   (broken)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connected successfully to Async Redis!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Two things I'm keeping:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Timeout vs refused is a routing hint, not a service hint.&lt;/strong&gt; When a service is provably up but the client times out, stop poking the service and start asking &lt;em&gt;where the packet actually goes&lt;/em&gt;. &lt;code&gt;ip route get &amp;lt;ip&amp;gt;&lt;/code&gt; is the fastest question you can ask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker's defaults live inside Hyper-V's range.&lt;/strong&gt; On any Windows/Hyper-V host, pin &lt;code&gt;daemon.json&lt;/code&gt; to &lt;code&gt;10.x&lt;/code&gt; on day one. Don't wait for the overlap to bite — it only bites &lt;em&gt;after&lt;/em&gt; a reboot re-rolls the subnet, which is the worst possible time to be surprised.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;docker exec ... PONG&lt;/code&gt; lied to me for a good ten minutes. It was answering over the container's own loopback, a network path the outside world never uses. The truth was one &lt;code&gt;ip route get&lt;/code&gt; away the whole time.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>redis</category>
      <category>networking</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Skill Writes Itself: Building Complex Claude Skills From Real Work</title>
      <dc:creator>Denis Morozov</dc:creator>
      <pubDate>Fri, 15 May 2026 21:35:20 +0000</pubDate>
      <link>https://dev.to/frozer/the-skill-writes-itself-building-complex-claude-skills-from-real-work-3hgm</link>
      <guid>https://dev.to/frozer/the-skill-writes-itself-building-complex-claude-skills-from-real-work-3hgm</guid>
      <description>&lt;p&gt;You know how it goes. You're deep in some code, implementing a new feature or reading about another cool thing you can do with your brain and AI power, when — BUZZ — the work chat blows up. 15 seconds later, you're on an emergency call with the top dogs asking what we can do about it.&lt;/p&gt;

&lt;p&gt;Sometimes it happens so fast that users never even notice the issue, chalking it up to a "network error" or "overheated device" — while deep in the Kibana logs, in the silence of traces and spans, the issue grows until it's finally caught by an alert and lands in your inbox as "This isn't an emergency, but it looks suspicious...". Some of you may have a first-class aggregation system that does the job for you, delivering the full picture along with your morning cappuccino. In my case, I have to read Kibana logs across different Kibana instances, check traces in Datadog, dig through the Wiki, JIRA, or even GitHub, and finally tie all the strings together.&lt;/p&gt;

&lt;p&gt;One day, filling out yet another investigation report — two-thirds of which were log excerpts showing the same broken workflow — it hit me: &lt;em&gt;"Why am I still doing this?"&lt;/em&gt; The investigation and deduction are the fun part. The tedious part is logging into every resource, defining the same time interval five times, copying and combining logs, and tracking down trace IDs (which can mutate as a message travels through different systems).&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick: don't design the SKILL, &lt;em&gt;discover&lt;/em&gt; it
&lt;/h2&gt;

&lt;p&gt;Here's what I want you to take away: &lt;strong&gt;don't sit down to design a complex SKILL from scratch. Do the work with the AI, then ask it to codify the workflow into a SKILL afterwards. The conversation is the spec.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This works because a SKILL is just a program written at a higher level of abstraction. And the easiest way to write that program is to &lt;em&gt;run it manually first&lt;/em&gt;, with the AI watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it played out
&lt;/h2&gt;

&lt;p&gt;I already had a Swiss Army knife of skills I use in regular work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a git reader (curl-based)&lt;/li&gt;
&lt;li&gt;an Atlassian Wiki skill (read/write Wiki pages)&lt;/li&gt;
&lt;li&gt;a JIRA skill (read/write tickets)&lt;/li&gt;
&lt;li&gt;a Datadog reader (query traces)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the next time an alert landed, instead of opening five tabs, I just started asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Check the logs for this time interval for %USER%."&lt;/em&gt;&lt;br&gt;
&lt;em&gt;"Analyze the Datadog traces for error responses and cross-reference them against the collected logs — find matches, highlight suspicious patterns."&lt;/em&gt;&lt;br&gt;
&lt;em&gt;"Build an issue timeline and list the systems involved."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And here's the kicker — the GitHub skill. Once you know the application version, you can pull the sources, find the exact module producing the error, point right to it in the report, and even run a simulation against the affected app to nail down the root cause.&lt;/p&gt;

&lt;p&gt;When the report was ready, instead of clearing the context, I asked: &lt;em&gt;"Create a new SKILL based on this conversation."&lt;/em&gt; Review, save, done. That's &lt;code&gt;issue-investigator&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From the first alert to a complete report: 5 minutes on a simple issue, a few hours on the gnarly PROD ones — end-to-end, with me reading the result instead of assembling it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep your skills sharp
&lt;/h2&gt;

&lt;p&gt;A SKILL built from one real investigation isn't finished — it's a v1. Every time you reuse it and spot a rough edge, polish it. That's how a SKILL stays sharp.&lt;/p&gt;

&lt;p&gt;And once it &lt;em&gt;is&lt;/em&gt; sharp — share it with your team. If you cut down the time you spend on tedious work, you boost your own efficiency. Spread that tool across your team and you boost everyone's. That's the multiplier you don't get from solving the problem alone.&lt;/p&gt;

&lt;p&gt;And when every team works this way, SKILLs stop being personal productivity hacks — they become the operating system of the company.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>Nuxt 3 Deployment: Automating Environment-Specific Builds with GitHub Actions</title>
      <dc:creator>Denis Morozov</dc:creator>
      <pubDate>Sun, 23 Feb 2025 22:39:28 +0000</pubDate>
      <link>https://dev.to/frozer/nuxt-3-deployment-automating-environment-specific-builds-with-github-actions-31h4</link>
      <guid>https://dev.to/frozer/nuxt-3-deployment-automating-environment-specific-builds-with-github-actions-31h4</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hi All,  &lt;/p&gt;

&lt;p&gt;Recently, I needed to prepare a deployment procedure for a Nuxt-based frontend. Based on my experience, I initially expected to create a single build that could be used across all environments — QA, UAT, and PROD. However, I discovered that I had to generate separate builds for each environment, each with its own &lt;code&gt;.env&lt;/code&gt; file defining the Strapi URL and other parameters. This limitation arose due to the &lt;code&gt;@nuxt/image&lt;/code&gt; library which does not support runtime configuration option for its plugins (we're using &lt;a href="https://strapi.io" rel="noopener noreferrer"&gt;Strapi&lt;/a&gt; to store images).  &lt;/p&gt;

&lt;p&gt;Different teams have different workflows—some prefer a single build deployed across all environments (&lt;em&gt;Build-Once, Deploy Everywhere&lt;/em&gt;), while others go with separate builds per environment. Both approaches have their &lt;em&gt;pros&lt;/em&gt; and &lt;em&gt;cons&lt;/em&gt;.  &lt;/p&gt;

&lt;p&gt;I prefer the first approach due to the following advantages:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Program behavior remains unchanged, ensuring consistency. If only configuration files or databases change, you can guarantee that something working in preproduction will work in production simply by ensuring those configurations match.
&lt;/li&gt;
&lt;li&gt;It's slightly more robust. You maintain only one "gold" build, reducing the risk of mistakenly deploying a dev version to QA, and so on.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, due to the limitation I mentioned above, I couldn't use this approach and had to venture into the unfamiliar waters of per-environment builds. Unfortunately, Nuxt’s documentation on this topic is unclear (see &lt;a href="https://nuxt.com/docs/getting-started/deployment" rel="noopener noreferrer"&gt;Nuxt Deployment Guide&lt;/a&gt;), focusing more on exact deployment or &lt;a href="https://nuxt.com/docs/getting-started/testing" rel="noopener noreferrer"&gt;testing procedures&lt;/a&gt; rather than workflow guidance for development teams.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Environment Setup
&lt;/h2&gt;

&lt;p&gt;To address this issue, I explored and documented a structured approach to handling environment-specific builds in Nuxt, aiming to bridge this gap in the official documentation.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Technology Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nuxt 3&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strapi&lt;/strong&gt; as the backend
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 18&lt;/strong&gt; for building and running
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt; for storing source code and artifacts
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Two environments: &lt;strong&gt;UAT&lt;/strong&gt; and &lt;strong&gt;PROD&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A changelog for each release
&lt;/li&gt;
&lt;li&gt;The ability to initiate a release by assigning a tag to a commit
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@nuxt/image&lt;/code&gt; plugin requires a statically injected (&lt;code&gt;.env&lt;/code&gt;) &lt;code&gt;STRAPI_URL&lt;/code&gt;, which cannot be placed in &lt;code&gt;runtimeConfig&lt;/code&gt; (See the &lt;a href="https://nuxt.com/docs/getting-started/configuration#runtimeconfig-vs-appconfig" rel="noopener noreferrer"&gt;Nuxt Runtime Config Docs&lt;/a&gt;)
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Workflow
&lt;/h2&gt;

&lt;p&gt;This workflow automates the build and release process by generating environment-specific builds and creating a changelog for each release.  &lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Triggering the Workflow&lt;/strong&gt;: The build and release process is triggered when a tag matching &lt;code&gt;v*&lt;/code&gt; is pushed. To deploy to UAT, for example, we assign a tag to the respective commit. The first tag (e.g., &lt;code&gt;v1.0.0&lt;/code&gt;) requires a manual &lt;code&gt;CHANGELOG.md&lt;/code&gt; generation via:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git log %YOUR_FIRST_COMMIT%..&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;{ env.TAG_NAME &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--pretty&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;format:&lt;span class="s2"&gt;"%h %s by %an"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please refer to the Git documentation for a &lt;a href="https://git-scm.com/docs/pretty-formats" rel="noopener noreferrer"&gt;detailed explanation of the output format&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Repository Permissions&lt;/strong&gt;: The workflow requires &lt;code&gt;write&lt;/code&gt; access to create releases and attach artifacts.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changelog Generation&lt;/strong&gt;: The difference between the current and previous tags is used to generate a meaningful changelog.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment-Specific Configuration&lt;/strong&gt;: GitHub Action Secrets store the Strapi backend URL for each environment, ensuring secure and correct configurations.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release Artifacts&lt;/strong&gt;: Each release includes two build artifacts, one for UAT and one for PROD, ensuring proper separation.
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;GitHub Actions Workflow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&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;v*'&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&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;artifact_package_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project61-nuxt&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;18.x&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;UAT&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;PROD&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@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;Save tag as environment variable&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "TAG_NAME=${GITHUB_REF#refs/tags/}" &amp;gt;&amp;gt; $GITHUB_ENV&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;Store artifact package name as environment variable&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo ARTIFACT_NAME=${{ env.artifact_package_name }}-${{ env.TAG_NAME }}-${{ matrix.environment }}.tar.gz &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use Node.js ${{ matrix.node-version }}&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@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="s"&gt;${{ matrix.node-version }}&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="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;yarn install --immutable --check-cache --non-interactive&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;Load Environment Variables&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;echo "STRAPI_URL=${{ secrets[format('{0}_STRAPI_URL', matrix.environment)] }}" &amp;gt; .env&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 Nuxt App&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;npx nuxt build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pack build artifacts&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;&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;tar zcvf ${{ env.ARTIFACT_NAME }} .output&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 Artifact&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@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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ARTIFACT_NAME }}&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;${{ env.ARTIFACT_NAME }}&lt;/span&gt;

  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&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;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@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;Download All Artifacts&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@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;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&lt;/span&gt;
          &lt;span class="na"&gt;merge-multiple&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Save tag as environment variable&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "TAG_NAME=${GITHUB_REF#refs/tags/}" &amp;gt;&amp;gt; $GITHUB_ENV&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;Store previous tag&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "PREV_TAG_NAME=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1))" &amp;gt;&amp;gt; $GITHUB_ENV&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;Create changelog&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;echo ${{ env.PREV_TAG_NAME }}..${{ env.TAG_NAME }}   &lt;/span&gt;
          &lt;span class="s"&gt;git log ${{ env.PREV_TAG_NAME }}..${{ env.TAG_NAME }} --pretty=format:"%h %s by %an" &amp;gt; CHANGELOG.md&lt;/span&gt;
          &lt;span class="s"&gt;ls -l dist/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Release&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;gh release create ${{ github.ref_name }} dist/*.tar.gz --title "Release ${{ github.ref_name }}" --notes-file CHANGELOG.md&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;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;This workflow streamlines the deployment process by automating build creation, versioning, and changelog generation. Each environment gets its own dedicated build, ensuring proper separation and accurate testing. By using GitHub Action Secrets, environment-specific configurations remain secure, and every release includes artifacts for both UAT and PROD.  &lt;/p&gt;

&lt;p&gt;Oh, and one final discovery—while generating changelogs, we realized that &lt;em&gt;some developers&lt;/em&gt; might need a refresher on writing meaningful commit messages. Turns out, "fix stuff" and "update" aren't the most helpful descriptions when trying to track changes! 😅&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>webdev</category>
      <category>cicd</category>
      <category>github</category>
    </item>
    <item>
      <title>Async Cache Service - you don't need "isLoading" anymore!</title>
      <dc:creator>Denis Morozov</dc:creator>
      <pubDate>Tue, 23 May 2023 02:07:27 +0000</pubDate>
      <link>https://dev.to/frozer/async-cache-service-1lj7</link>
      <guid>https://dev.to/frozer/async-cache-service-1lj7</guid>
      <description>&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;Simple Javascript library to manage asynchronous cache/storage in app/service.&lt;/p&gt;

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

&lt;p&gt;The best category matches my project is &lt;strong&gt;Wacky Wildcards&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  App Link
&lt;/h3&gt;

&lt;p&gt;You can find library page and package here - &lt;a href="https://www.npmjs.com/package/async-cache-service"&gt;Async Cache Service on NPM&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  Description
&lt;/h3&gt;

&lt;p&gt;The asynchronous cache service is designed to handle the storage and retrieval of data in a non-blocking manner. When data is being loaded into the cache, the service returns a Promise object to the requester. This Promise serves as a placeholder or notification that the data is currently in the process of being fetched.&lt;/p&gt;

&lt;p&gt;Once the data loading process is complete and the data is successfully retrieved, the cache service resolves all previously issued Promises associated with that specific data. By resolving the Promises, the cache service effectively provides the updated and complete data to all subscribers or requesters who were waiting for it.&lt;/p&gt;

&lt;h4&gt;
  
  
  How to use
&lt;/h4&gt;

&lt;p&gt;Typescript Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsyncCacheService&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;async-cache-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// by default - the cache is never expire&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cacheService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AsyncCacheService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;// with 5min expiration&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cacheService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AsyncCacheService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// as a dependency with 15min expiration&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;SomeDataService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;cacheService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AsyncCacheService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="nx"&gt;_000&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;Javascript Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;AsyncCacheService&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;async-cache-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;DataService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// cache expires in 15min&lt;/span&gt;
  &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AsyncCacheService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cacheService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cacheService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cacheService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isExpired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cacheService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// retrieve data from somewhere&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/some/resource&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cacheService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cacheService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flushItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cacheService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the &lt;em&gt;getData&lt;/em&gt; method of DataService is called, it returns a Promise, and DataService initiates the data load. During this process, all incoming calls to &lt;em&gt;getData&lt;/em&gt; will receive Promises that resolve after the data load. In other words, we eliminate the necessity to have a well-known &lt;em&gt;isLoading&lt;/em&gt; flag in our DataService and provide an elegant way to handle multiple data requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Link to Source Code
&lt;/h3&gt;

&lt;p&gt;You can find library sources here - &lt;a href="https://github.com/frozer/async-cache-service"&gt;Async Cache Service on Github&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Permissive License
&lt;/h3&gt;

&lt;p&gt;This project is under &lt;a href="https://github.com/frozer/async-cache-service/blob/main/LICENSE"&gt;MIT License&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Background (What made you decide to build this particular app? What inspired you?)
&lt;/h2&gt;

&lt;p&gt;In the preceding article &lt;a href="https://dev.to/frozer/vanilla-js-data-cache-service-1ei2"&gt;Vanilla JS data cache service&lt;/a&gt;, I emphasized the issue of asynchronous data cache and outlined a potential solution. However, after thoughtful consideration, I opted to integrate the suggested approach into a compact Javascript library, accompanied by comprehensive unit tests and leveraging Github Actions for complete CI/CD workflow - encompassing build -&amp;gt; test -&amp;gt; package -&amp;gt; deploy. The resultant library, named "async-data-service", introduces an efficient method to manage asynchronous cache/storage operations with minimal developer overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I built it (How did you utilize GitHub Actions or GitHub Codespaces? Did you learn something new along the way? Pick up a new skill?)
&lt;/h3&gt;

&lt;p&gt;Well, I used TypeScript (love it!) and Rollup for bundling. For my projects, I prefer to use the &lt;a href="https://nx.dev"&gt;Nx build system&lt;/a&gt;it provides me with a lot of pre-defined tools and allows me to focus on what I do. For unit tests, I used the Jest framework. The real challenge for me in this project was to utilize GitHub Actions for NPM publishing. &lt;/p&gt;

&lt;p&gt;The most available resources describe the publishing process in a pretty straightforward way, like (taken from official :&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish Package to npmjs&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;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;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="c1"&gt;# Setup .npmrc file to publish to npm&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;16.x'&lt;/span&gt;
          &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://registry.npmjs.org'&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 publish&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;NODE_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NPM_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which isn't really helpful. How does "npm ci" relate to "npm publish"? What is the difference between a "published" release and a "created" release? And finally, where are my built artifacts?&lt;/p&gt;

&lt;p&gt;So, I need to find answers. The first thing I resolved was the question regarding the difference between "created" and "published" events for releases. Actually, the "created" event is emitted only for draft releases, while the "published" event is emitted for non-draft releases. Therefore, I will use the "published" event in my Github Actions.&lt;/p&gt;

&lt;p&gt;Next, I have to consider that Nx puts build artifacts into the "dist/packages/async-cache-service" folder. Therefore, I need to run "npm publish" from there. As a result of my GitHub Actions efforts, I implemented two actions - "push" and "publish".&lt;/p&gt;

&lt;p&gt;The "push" action is used to handle the push event of a pull request and performs the build and test of the proposed changes:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Node.js CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&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;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

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

    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;18.x&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;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;Use Node.js ${{ matrix.node-version }}&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@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="s"&gt;${{ matrix.node-version }}&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 run build --if-present&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;The "publish" action is used to handle the release publish event and performs the build, test, and publish steps for the release in two steps:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish Package to npmjs&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;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;18.x&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;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;Use Node.js ${{ matrix.node-version }}&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@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="s"&gt;${{ matrix.node-version }}&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="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm ci&lt;/span&gt;
          &lt;span class="s"&gt;npm run build --if-present&lt;/span&gt;
          &lt;span class="s"&gt;npm 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;Archive production artifacts&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;artifact&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist/packages/async-cache-service&lt;/span&gt;

  &lt;span class="na"&gt;download&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&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;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download build step artifact&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@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;artifact&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.x'&lt;/span&gt;
          &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://registry.npmjs.org'&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 publish&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;NODE_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NPM_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, this action contains two steps - &lt;strong&gt;build&lt;/strong&gt; and &lt;strong&gt;publish&lt;/strong&gt;. The &lt;strong&gt;build&lt;/strong&gt; step performs the build, test, and stores the build artifacts into the Github storage. The &lt;strong&gt;publish&lt;/strong&gt; step retrieves the just created build from the build node and publishes it into the NPM registry.&lt;/p&gt;

&lt;p&gt;In summary, I find myself very interested in using GitHub Actions for my current and future projects. I love the way it works. My next step is to adopt GitHub Actions for another project I'm working on, written in C.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/59319281/github-action-different-between-release-created-and-published"&gt;Github Action different between release created and published&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts"&gt;Storing workflow data as artifacts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages"&gt;Publishing Node.js packages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>githubhack23</category>
      <category>nx</category>
      <category>typescript</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Demystifying Nx's "dependsOn" configuration</title>
      <dc:creator>Denis Morozov</dc:creator>
      <pubDate>Sun, 07 May 2023 02:07:18 +0000</pubDate>
      <link>https://dev.to/frozer/demystifying-nxs-dependson-configuration-f4b</link>
      <guid>https://dev.to/frozer/demystifying-nxs-dependson-configuration-f4b</guid>
      <description>&lt;p&gt;We're using Nx in our multirepos to build our Node/Nest apps, as well as Angular. It was a long story how we migrated from Angular Workspace repo for web apps, and single repos for NodeJS/Nest, but in this article I'd like to focus on "dependsOn" property of &lt;code&gt;project.json&lt;/code&gt; and how we treat it.&lt;/p&gt;

&lt;p&gt;Nx documentation contains a good portion of "dependsOn" description - &lt;a href="https://nx.dev/reference/project-configuration#dependson"&gt;https://nx.dev/reference/project-configuration#dependson&lt;/a&gt; but as for me it still not clear how does "dependsOn" inside "targetDefaults" interact with project-level "dependsOn".&lt;/p&gt;

&lt;p&gt;In our project we're utilizing some shared libraries which can be used for Angular apps and for NodeJS/Nest apps as well, i.e. some models definitions and so on. Thus, it is important that those libraries should be built before other apps. That's why we included libraries definitions into "targetDefaults". From other hand we have got some script which need to be run before build, so we create a separate task inside &lt;code&gt;project.json&lt;/code&gt; to describe it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; "injectBuildVersion": {
   "executor": "nx/run-script",
   "options": {
     "script": "inject-build-version some-app"
   }
 }
 "targetDefaults": {
   "build": {
     "dependsOn": "some-lib"
   }
 },
 "dependsOn": ["injectBuildVersion"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as a result the application was failing to build, because library wasn't built yet. But we noticed that "injectBuildVersion" was executing correctly.&lt;/p&gt;

&lt;p&gt;The reason of such behavior is that project-level "dependsOn" overrides "targetDefaults". Moreover, this behavior already described somewhere in Nx issues :-) To make it work, we have to include a special dependency "^build" into project-level "dependsOn" property, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; "injectBuildVersion": {
   "executor": "nx/run-script",
   "options": {
     "script": "inject-build-version some-app"
   }
 }
 "targetDefaults": {
   "build": {
     "dependsOn": "some-lib"
   }
 },
 "dependsOn": ["injectBuildVersion", "^build"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, everything works as expected, build version value (which comes from Jenkins) injects into &lt;code&gt;environment.ts&lt;/code&gt; file, and then Nx performs the build with all necessary dependencies. Good job, Nx!  &lt;/p&gt;

</description>
      <category>nx</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Singletons in JS And How To Use Them</title>
      <dc:creator>Denis Morozov</dc:creator>
      <pubDate>Wed, 07 Sep 2022 02:20:18 +0000</pubDate>
      <link>https://dev.to/frozer/singletons-in-js-and-how-to-use-them-2abb</link>
      <guid>https://dev.to/frozer/singletons-in-js-and-how-to-use-them-2abb</guid>
      <description>&lt;p&gt;Hi Team,&lt;/p&gt;

&lt;p&gt;Today, I'd like to tell about singletons and how we are using them on daily-basis. &lt;br&gt;
So, singleton pattern allows us to create single instance of particular class. For which purposes  can it be helpful? We are using them for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;data interchange services, which take care about data transferring to/from underlying services&lt;/li&gt;
&lt;li&gt;data processing services, which can be instantiated only once on application start&lt;/li&gt;
&lt;li&gt;cache services&lt;/li&gt;
&lt;li&gt;testing purposes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, we decided to continue with previously described Cache Service (&lt;a href="https://dev.to/frozer/vanilla-js-data-cache-service-1ei2"&gt;https://dev.to/frozer/vanilla-js-data-cache-service-1ei2&lt;/a&gt;), for some reasons we don't need multiple &lt;br&gt;
cache services in our app, so how we are going to create a singletone?&lt;/p&gt;

&lt;p&gt;Let's start with very basic code to create a singletone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class SingletoneService {
  static instance;

  getInstance(args) {
    if (!SingletoneService.instance) {
      SingletoneService.instance = new SingletoneService(...args);
    }

    return SingletoneService.instance;
  }

  constructor(args) {
    // do something with args
  }

  doSomething() {
    // do something
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, how does it work? In the main program we can create instance of singleton by calling this &lt;em&gt;getInstance&lt;/em&gt; method with arguments (if we need them):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function main() {
  const instance = SingletoneService.getInstance();

  instance.doSomething();
}
main();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By calling the &lt;em&gt;getInstance&lt;/em&gt; method it checks for static &lt;strong&gt;instance&lt;/strong&gt; field value of class SingletoneService, and then instantiate a new object based on that class using the &lt;em&gt;new&lt;/em&gt; keyword. If it exists, it simply returns the existing instance.&lt;/p&gt;

&lt;p&gt;Now, once the draft implementation is done, try to move this functionality into our CacheService:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class SomeServiceWithDataCache {
  static instance;

  getInstance(args) {
    if (!SomeServiceWithDataCache.instance) {
      SomeServiceWithDataCache.instance = new SomeServiceWithDataCache(...args);
    }

    return SomeServiceWithDataCache.instance;
  }

  constructor() {
    this.cache = {
      isLoading: false,
      expire: 0,
      data: null
    };
    this.cacheSubscriptions = [];
  }
  ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, you can use singletones to inject dependency to your class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class DataProcessingService {
  // use existing DataCacheService instance by default
  constructor(cacheService = DataCacheService.getInstance()) {
    // do something in constructor)
  }
  ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which allows you to implement code isolation and Open-Closed principle.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>beginners</category>
      <category>oop</category>
    </item>
    <item>
      <title>Vanilla JS data cache service</title>
      <dc:creator>Denis Morozov</dc:creator>
      <pubDate>Mon, 11 Jul 2022 01:35:09 +0000</pubDate>
      <link>https://dev.to/frozer/vanilla-js-data-cache-service-1ei2</link>
      <guid>https://dev.to/frozer/vanilla-js-data-cache-service-1ei2</guid>
      <description>&lt;p&gt;Hi Team,&lt;/p&gt;

&lt;p&gt;Some time ago I faced an interesting task to solve, and today I'd like to share what I found. Let's imagine that we have a service which needs to load some data (sounds familiar, huh?), &lt;/p&gt;

&lt;p&gt;Also we've got several "customers" which utilize the data retrieval method from this service, and for first look all works fine, but when I opened the Developers Console, I was wondering how much duplicate data requests we got there! &lt;/p&gt;

&lt;p&gt;Here is our initial implementation of that service (I rewrote it to be more general):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// let's assume we have some network slowness there...
const delayedDataFetch = () =&amp;gt; new Promise((resolve) =&amp;gt;
  setTimeout(() =&amp;gt; resolve([1, 2, 3]), 2000)
);

class SomeService {
  async getData() {
    return await delayedDataFetch();
  }
}

// ...
// emulation of parallel requests for the same data
function main() {
  const service = new SomeService();
  await Promise.all([
    service.getData(1),
    service.getData(2)
  ]).then(
    ([res1, res2]) =&amp;gt; {
      // receives correct data
      console.log(`1. ${JSON.stringify(res1)}`);
      // receives correct data once again, 
      // from the second call to delayedDataFetch
      // and I want to get rid of that second data call to server
      console.log(`2. ${JSON.stringify(res2)}`);
    }
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, my first thought was to implement a some little cache for such data:&lt;br&gt;
&lt;/p&gt;

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

class SomeService {
  constructor() {
    this.cache = {
      isLoading: false,
      data: null,
      // current timestamp plus expiration delay
      expire: 0
    };
  }

  // added sequenceId for more clarity
  async getData(sequenceId) {
    console.log(`Received ${sequenceId} request`);

    // if cache is expired and isLoading is false 
    // - initiate data update from server
    if (this.cache.expire &amp;lt; new Date().getTime() &amp;amp;&amp;amp; !this.cache.isLoading) {
      this.cache.isLoading = true;

      this.cache = {
        isLoading: false,
        data: await delayedDataFetch(),
        expire: new Date().getTime() + CACHE_EXPIRATION_TIME_MS
      };
    }

    console.log(`Response ${sequenceId} with data`);
    return this.cache.data;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works! At least, as I can see from Network Tab the number of networks requests reduced to one. Looks like I can submit this code to repo. &lt;/p&gt;

&lt;p&gt;But... hold on, I just started to receive errors from other parts of applications, and the main issue here, that they are different from time to time. Quick searching gives me a new problem - the new code returns null data from time to time. &lt;/p&gt;

&lt;p&gt;So, this simple approach didn't solve the problem completely. Let's look closely to this code of &lt;code&gt;getData&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  // a first request switch isLoading to true, and...
  if (this.cache.expired &amp;lt; new Date().getTime() &amp;amp;&amp;amp; !this.cache.isLoading) {
    this.cache.isLoading = true;
    // ... do something
  }

  // the second one simply receives empty data... 
  console.log(`Response ${sequenceId} with data`);
  return this.cache.data;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gotcha! I found! But how can we say our "customers" that they need to wait? Of course, I can switch to RxJS f.e., but I don't want to bubble up the size or our bundle, and I was eager to reach the goal using more vanilla approach.&lt;/p&gt;

&lt;p&gt;I started about to return an another Promise to the second response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  // a first request switch isLoading to true, and...
  if (this.cache.expire &amp;lt; new Date().getTime() &amp;amp;&amp;amp; !this.cache.isLoading) {
    this.cache.isLoading = true;
    // ... do something
  }

  if (this.cache.isLoading) {
    // it should return something to client in such cases, 
    // which "something" should be resolved back to refreshed data
    console.log(`Response ${sequenceId} with unresolved promise`);
    return new Promise(resolve =&amp;gt; ???);
  }

  // the second one simply receives empty data... 
  console.log(`Response ${sequenceId} with data`);
  return this.cache.data;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I was thinking about how I can resolve it, and a flash brighten my brain :-) - I need a simple PUB/SUB!&lt;br&gt;
I decided to add an additional subscribers pool into &lt;code&gt;SomeService&lt;/code&gt; class and renamed it to &lt;code&gt;SomeServiceWithDataCache&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class SomeServiceWithDataCache {
  constructor() {
    this.cache = {
      isLoading: false,
      expire: 0,
      data: null
    };
    this.cacheSubscriptions = [];
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and change the code of &lt;code&gt;getData&lt;/code&gt; method accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  async getData(sequenceId) {
    console.log(`Received ${sequenceId} request`);
    if (this.cache.expire &amp;lt; new Date().getTime() &amp;amp;&amp;amp; !this.cache.isLoading) {
      this.cache.isLoading = true;

      this.cache = {
        isLoading: false,
        data: await delayedDataFetch(),
        expire: new Date().getTime() + CACHE_EXPIRATION_TIME_MS
      };

      // once we receive data - iterate over subscribers pool, 
      // and resolve each with received data, and finally drop
      // subscriptions
      await Promise.all(
        this.cacheSubscriptions.map((res) =&amp;gt; res(this.cache.data))
      ).then(() =&amp;gt; (this.cacheSubscriptions = []));
    }

    if (this.cache.isLoading) {
      console.log(`Response ${sequenceId} with unresolved promise`);
      // push the promise resolve into subscribers pool
      return new Promise((resolve) =&amp;gt; this.cacheSubscriptions.push(resolve));
    }

    console.log(`Response ${sequenceId} with data`);
    return this.cache.data;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, how it works - it receives the first data request from client, founds that internal data cache is expired, and initiates data loading. For the second request it returns the unresolved Promise, and put the resolver function into the subscribers pool. Once it receives data from server it iterates through the subscribers pool and call resolver with an actual data. That's it.&lt;/p&gt;

&lt;p&gt;You can find complete code for the solution on CodeSandBox - &lt;a href="https://codesandbox.io/s/simple-data-caching-service-cds83d"&gt;https://codesandbox.io/s/simple-data-caching-service-cds83d&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>SCRUM Retrospective meeting is for what?</title>
      <dc:creator>Denis Morozov</dc:creator>
      <pubDate>Sat, 19 Sep 2020 18:19:03 +0000</pubDate>
      <link>https://dev.to/frozer/scrum-retrospective-meeting-for-what-f7d</link>
      <guid>https://dev.to/frozer/scrum-retrospective-meeting-for-what-f7d</guid>
      <description>&lt;p&gt;Hi Devs,&lt;/p&gt;

&lt;p&gt;Most of us tackled with SCRUM at least at once. As for me, I do my job about five years using the SCRUM methology. It helpful, it allows us to deliver features and fixes more predictive and transparent, and sometimes, in time :-) you know what I mean. I was a SCRUM master, team leader, ordinal developer, chief... and know what? I still can't understand why we need retrospective. &lt;/p&gt;

&lt;p&gt;Last Friday we had one, and our SCRUM master opened a Word document with empty three-column table and asked us the question - "John/Sam/Dan, what you think about last sprint, what was good, what we need to change and what is ugly?". You know that question and I always in trouble what I should to answer. Look, I have a lot of JIRA's, I communicated well with others (BAs, QAs) to get them to solve, and I solved them. Nothing to say - ALL GOOD! And everyone of us said nothing, because - all works well. Right after the meeting, our SCRUM master said that my team-mates "aren't evolved into product". Hey dude, these guys deliver features/fixes every two week in-time and mostly error-free, you can't judge them for the silence on the Retrospective meeting!&lt;/p&gt;

&lt;p&gt;If I can't communicate with someone, have hardware problem, or my IDE license has been expired - I don't need to wait until end of the sprint, to tell about it on the retrospective, otherwise I won't do my job well.&lt;/p&gt;

&lt;p&gt;As SCRUM methology said - "discuss problems and circumstances with other team-mates..." Cool, but I have time to discuss about on the daily meetings.&lt;/p&gt;

&lt;p&gt;So, what do you think about Retrospective? How do we need to cook it well?&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
