<?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: Marko Anastasov</title>
    <description>The latest articles on DEV Community by Marko Anastasov (@markoa).</description>
    <link>https://dev.to/markoa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F45691%2F6e7af127-5b42-4e1c-981c-899265c8f433.jpg</url>
      <title>DEV Community: Marko Anastasov</title>
      <link>https://dev.to/markoa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/markoa"/>
    <language>en</language>
    <item>
      <title>An “n8n for DevOps” Control Plane - Superplane</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Tue, 27 Jan 2026 07:23:17 +0000</pubDate>
      <link>https://dev.to/superplane/an-n8n-for-devops-control-plane-superplane-54i4</link>
      <guid>https://dev.to/superplane/an-n8n-for-devops-control-plane-superplane-54i4</guid>
      <description>&lt;p&gt;&lt;em&gt;We’re launching Superplane, an open-source control plane similar to n8n that orchestrates your entire DevOps workflow.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If this sounds interesting, please &lt;a href="https://github.com/superplanehq/superplane" rel="noopener noreferrer"&gt;give us a star on GitHub&lt;/a&gt; ⭐ it helps us grow the project.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Superplane is Tackling
&lt;/h2&gt;

&lt;p&gt;Deploying to production is rarely a straightforward process.&lt;/p&gt;

&lt;p&gt;You’ve got CI/CD pipelines in to manage, release management in Jira, incident response in PagerDuty, monitoring in &lt;a href="https://github.com/SigNoz/signoz" rel="noopener noreferrer"&gt;SigNoz&lt;/a&gt;, notifications in Slack. And somewhere in between, you’re manually coordinating all of them.&lt;/p&gt;

&lt;p&gt;You can’t simply write a script that will handle all the cases of a complex workflow, where one thing has to happen after this other thing is done running. Creating a mental map of these workflows is also no easy task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DevOps workflows are fragmented across a dozen tools, and we built the first control plane to orchestrate them all. Yes, we were inspired by n8n a lot.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The “n8n for DevOps” Vision
&lt;/h2&gt;

&lt;p&gt;If you’ve used &lt;a href="https://n8n.io/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, you know the magic: visual workflows, event-driven triggers, integrations with everything. &lt;/p&gt;

&lt;p&gt;But here’s the thing, n8n is built for &lt;em&gt;general&lt;/em&gt; automation. It’s amazing for connecting your CRM to your email, or syncing data between APIs. But when it comes to DevOps processes like deployments and progressive rollouts, you still have to manage custom nodes, states, and hope your webhooks don’t timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What if there was an n8n specifically for DevOps?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A platform where you define your operational workflows once, and they just… &lt;em&gt;work&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;With Superplane, you define your services, environments, releases, incidents, and signals in one place. This way it’s easy to understand how your system behaves and what the exact workflow is. The tribal knowledge of your DevOps team gets captured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We’d love it if you could please &lt;a href="https://github.com/superplanehq/superplane" rel="noopener noreferrer"&gt;give us a star on GitHub&lt;/a&gt; ⭐ as it helps us sustain the project.&lt;/strong&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What Makes Superplane Different
&lt;/h2&gt;

&lt;p&gt;Superplane isn’t trying to replace your CI/CD, your monitoring, or your incident management tools. Instead, it’s the &lt;strong&gt;control plane&lt;/strong&gt; that orchestrates them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Event-driven Workflows
&lt;/h3&gt;

&lt;p&gt;Superplane listens to events from your existing tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI pipeline completes → trigger workflow&lt;/li&gt;
&lt;li&gt;Schedule hits → run maintenance task&lt;/li&gt;
&lt;li&gt;Webhook arrives → process and route&lt;/li&gt;
&lt;li&gt;Alert fires in PagerDuty → start incident response
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fboxgn4ji1rvgn2cu1bcc.png" alt="Superplane dashboard" width="800" height="530"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You define workflows visually (like n8n), but with DevOps-specific primitives built in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational Context
&lt;/h3&gt;

&lt;p&gt;Unlike generic automation tools, Superplane understands DevOps concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Approval gates&lt;/strong&gt;: Require human approval before proceeding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy checks&lt;/strong&gt;: Enforce business hours, deployment windows, team policies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progressive rollouts&lt;/strong&gt;: Deploy in stages with verification between each&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-service coordination&lt;/strong&gt;: Wait for multiple repos/services, then fan-in&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  Self-Hosted, Open Source
&lt;/h2&gt;

&lt;p&gt;Superplane runs on your infrastructure. You own your workflows and data, everything is tailored to self-hosting needs.&lt;/p&gt;

&lt;p&gt;After months of assembling the team and building this from ground up, we’re finally ready to share Superplane with the world. We’re launching early, we’re moving fast, and there are rough edges. But we believe it’s ready for teams to start using it across different organizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability and State Management
&lt;/h2&gt;

&lt;p&gt;Every DevOps engineer reading this must be thinking: &lt;em&gt;"This sounds nice, but what happens when things go wrong?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These are the hard problems in orchestration, and we've built Superplane to handle them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Durable State Persistence
&lt;/h3&gt;

&lt;p&gt;Every workflow execution is stored in PostgreSQL. Events flow through the system with tracked states (&lt;code&gt;pending&lt;/code&gt; → &lt;code&gt;routed&lt;/code&gt; → &lt;code&gt;finished&lt;/code&gt;), and each node execution records its input, output, and status. If Superplane restarts, nothing is lost, it picks up where it left off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configurable Retries for HTTP Requests
&lt;/h3&gt;

&lt;p&gt;The HTTP component supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configurable retry count&lt;/strong&gt;: Set how many times to retry failed requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two timeout strategies&lt;/strong&gt;: &lt;code&gt;fixed&lt;/code&gt; (same timeout each attempt) or &lt;code&gt;exponential&lt;/code&gt; (timeout doubles each attempt, capped at 120 seconds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry metadata tracking&lt;/strong&gt;: Each attempt records its status, so you can see exactly what happened&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  End-to-End Event History
&lt;/h3&gt;

&lt;p&gt;Every event and execution is logged with full context like inputs, outputs, timestamps, and state transitions. When something goes wrong, you can trace exactly what happened, when, and why.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspectable Workflow Runs
&lt;/h3&gt;

&lt;p&gt;The control plane UI lets you inspect runs, see status at each step, and understand where things are stuck or failed. No more digging through logs across 5 different systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Workflows for Running Superplane in Production
&lt;/h2&gt;

&lt;p&gt;Here's what teams are actually building with Superplane:&lt;/p&gt;

&lt;h3&gt;
  
  
  Policy-Gated Production Deploy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI finishes green
  → Check: Is it business hours? (No → hold)
  → Check: Is on-call engineer available? (No → notify and wait)
  → Require: Product manager approval
  → Trigger: Production deployment
  → Verify: Health checks pass
  → Notify: Team in Slack

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All in one workflow. No manual coordination needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Progressive Delivery
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deploy to 10% of traffic
  → Wait 5 minutes
  → Check: Error rate &amp;lt; 0.1%? (Yes → continue, No → rollback)
  → Deploy to 30% of traffic
  → Wait 5 minutes
  → Check: Error rate &amp;lt; 0.1%? (Yes → continue, No → rollback)
  → Deploy to 60% of traffic
  → Wait 5 minutes
  → Check: Error rate &amp;lt; 0.1%? (Yes → continue, No → rollback)
  → Deploy to 100% of traffic

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automated progressive rollout with automatic rollback on failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  "First 5 Minutes" Incident Triage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incident created in PagerDuty
  → Parallel: Fetch recent deploys (last 2 hours)
  → Parallel: Fetch health metrics (error rate, latency)
  → Parallel: Check recent code changes
  → Combine: Generate evidence pack
  → Create: GitHub issue with context
  → Notify: On-call engineer with summary

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automated incident response that gives you context &lt;em&gt;before&lt;/em&gt; you start debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Run Superplane
&lt;/h2&gt;

&lt;p&gt;Superplane is self-hosted. You can run it on a single host or on Kubernetes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick start:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull ghcr.io/superplanehq/superplane-demo:stable
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="nt"&gt;-v&lt;/span&gt; spdata:/app/data &lt;span class="nt"&gt;-ti&lt;/span&gt; ghcr.io/superplanehq/superplane-demo:stable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; and start building workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production installation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://superplane.com/docs/installation/single-host" rel="noopener noreferrer"&gt;Single Host Installation&lt;/a&gt; - Deploy on AWS EC2, GCP Compute Engine, or other cloud providers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://superplane.com/docs/installation/kubernetes" rel="noopener noreferrer"&gt;Kubernetes Installation&lt;/a&gt; - Deploy on GKE, EKS, or any Kubernetes cluster&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚀 Try Superplane Now
&lt;/h2&gt;

&lt;p&gt;Superplane is open source (Apache 2.0), built by developers, for developers. No vendor lock-in. No per-seat pricing. Our workflows simply work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://github.com/superplanehq/superplane" rel="noopener noreferrer"&gt;Star us on GitHub&lt;/a&gt; to support the launch and stay updated!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Join the community:&lt;/strong&gt; &lt;a href="https://discord.gg/KC78eCNsnw" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; | &lt;a href="https://x.com/superplanehq" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://superplane.com/" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>opensource</category>
      <category>cloud</category>
      <category>automation</category>
    </item>
    <item>
      <title>You Need To Change The Whole Assembly Line</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Wed, 21 Jan 2026 12:59:52 +0000</pubDate>
      <link>https://dev.to/markoa/you-need-to-change-the-whole-assembly-line-ill</link>
      <guid>https://dev.to/markoa/you-need-to-change-the-whole-assembly-line-ill</guid>
      <description>&lt;p&gt;In the DevOps Paradox episode &lt;em&gt;&lt;a href="https://www.devopsparadox.com/episodes/2026-the-year-of-discovery-332/" rel="noopener noreferrer"&gt;2026 — The Year of Discovery&lt;/a&gt;&lt;/em&gt;, Viktor and Darin talk about how software work is changing, but the bottlenecks aren’t where people think.&lt;/p&gt;

&lt;p&gt;A few quotes are basically a version of why we &lt;a href="https://markoanastasov.com/signals/starting-superplane" rel="noopener noreferrer"&gt;started SuperPlane&lt;/a&gt;. My comments below.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You cannot just give developers, here’s AI. Everything else stays the same… you need to change the whole assembly line.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most organizations are currently applying AI to optimize the coding process.&lt;/p&gt;

&lt;p&gt;The constraint will simply move downstream: releases, verification, coordination, incident handling.&lt;/p&gt;

&lt;p&gt;If you don’t address the system, you get faster queues, not faster outcomes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You have a platform that you can say, hey, give me a database, and you get that database. And that database is done based on your company's best practices, rules, policies [...] ask me questions that I need to answer and don't ask anything more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is what engineers are actually asking for: outcomes, and systems that hide incidental complexity without hiding state.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;People simply don’t want to… relinquish part of their power… [and] don’t want to introduce randomness in production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the hard part. It’s not so much a capability problem as much it's a trust problem. People will only use automation in production when it’s controllable, auditable, and fails in predictable ways.&lt;/p&gt;

&lt;p&gt;To get there, I think we need a control plane that makes cross-tool operational workflows explicit and inspectable: what triggered, what ran, what it touched, and what evidence it produced. If you can’t explain a change, you can’t safely automate it.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>Cloudflare's November 18 Outage – A Continuous Delivery Perspective</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Thu, 27 Nov 2025 16:19:03 +0000</pubDate>
      <link>https://dev.to/markoa/cloudflares-november-18-outage-a-continuous-delivery-perspective-4e69</link>
      <guid>https://dev.to/markoa/cloudflares-november-18-outage-a-continuous-delivery-perspective-4e69</guid>
      <description>&lt;p&gt;On November 18th, 2025, Cloudflare had what they describe as their worst outage since 2019.&lt;/p&gt;

&lt;p&gt;It didn't start with a cyber attack or massive hardware failure. It started with a database permissions change.&lt;/p&gt;

&lt;p&gt;This is &lt;em&gt;exactly&lt;/em&gt; the sort of thing Continuous Delivery is supposed to make safe. So since &lt;a href="https://blog.cloudflare.com/18-november-2025-outage/" rel="noopener noreferrer"&gt;the post-mortem&lt;/a&gt; came out I wanted to analyze it through a CI/CD lens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary of what happened
&lt;/h3&gt;

&lt;p&gt;Around 11:20 UTC, Cloudflare’s network started returning a huge spike of HTTP 5xx errors for traffic flowing through their core network. &lt;a href="https://www.bbc.com/news/articles/c629pny4gl7o" rel="noopener noreferrer"&gt;Users all over the Internet&lt;/a&gt; started seeing Cloudflare error pages instead of the sites they were trying to visit.&lt;/p&gt;

&lt;p&gt;The root cause was a change to how one of their ClickHouse database clusters handled permissions and metadata.&lt;/p&gt;

&lt;p&gt;Here’s the rough chain of events:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cloudflare deployed a change to ClickHouse permissions and metadata visibility. That change meant queries against system metadata suddenly started returning &lt;em&gt;more rows&lt;/em&gt; than before — including underlying tables in another schema (&lt;code&gt;r0&lt;/code&gt;) where previously they only saw the &lt;code&gt;default&lt;/code&gt; database.&lt;/li&gt;
&lt;li&gt;One of the systems that depended on that metadata was Cloudflare’s Bot Management feature file generator. It queried &lt;code&gt;system.columns&lt;/code&gt; to assemble the list of features used by a machine learning model to score bots. That query didn’t filter by database name, so after the change it suddenly saw roughly double the number of columns, and produced a much larger feature file.&lt;/li&gt;
&lt;li&gt;That feature file is shipped out to Cloudflare’s edge network every few minutes. The bots module in their core proxy expects the number of features to be below a hard limit (200) and preallocates memory accordingly. The new, larger file exceeded that limit. The Rust code hit the limit and panicked, leading to 5xx responses from the core proxy.&lt;/li&gt;
&lt;li&gt;Because this file is generated every five minutes from a distributed ClickHouse cluster that was being gradually updated, the system actually went up and down for a bit: sometimes a good config, sometimes a bad one, depending on which node produced it. Eventually all nodes produced the bad file, and the failure became stable.&lt;/li&gt;
&lt;li&gt;Downstream systems that depend on the core proxy – things like Workers KV and Access – were also impacted until they were temporarily rerouted or bypassed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Main impact started around 11:28, substantial mitigation began around 14:30, and everything was fully resolved by 17:06 UTC.&lt;/p&gt;

&lt;p&gt;So: a change to database permissions → bigger config file → hard limit exceeded → proxy panics → global outage.&lt;/p&gt;

&lt;p&gt;That’s a &lt;strong&gt;change management story&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  This is why Continuous Delivery exists
&lt;/h3&gt;

&lt;p&gt;The primary cause of production failures is change. Continuous delivery is about making change boring.&lt;/p&gt;

&lt;p&gt;If your system is updated frequently then you must assume that &lt;strong&gt;any change is guilty until proven innocent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;From a CD perspective, a few big themes jump out:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Config is code
&lt;/h4&gt;

&lt;p&gt;The feature file that broke things isn’t some harmless data; it &lt;em&gt;is executable behaviour&lt;/em&gt;. It changes what the bot-scoring model does and it clearly has operational consequences. That should be treated with the same discipline as application code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stored and versioned in source control&lt;/li&gt;
&lt;li&gt;Validated in pipelines&lt;/li&gt;
&lt;li&gt;Subject to contract tests with the consumers (the proxy module)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Contracts between systems
&lt;/h4&gt;

&lt;p&gt;There is an implicit contract here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Producer: “I will generate a feature file.”&lt;/li&gt;
&lt;li&gt;Consumer: “I can safely handle up to 200 features.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That contract wasn’t explicitly enforced at integration boundaries. The producer didn’t know that exceeding 200 features would crash the consumer, and the consumer effectively said: &lt;em&gt;“If this assumption is broken, I will simply panic.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In a good CD setup, we encode these contracts as tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“If the feature file has &amp;gt; 200 features, the pipeline fails.&lt;/li&gt;
&lt;li&gt;Or, at runtime: “If it’s too big, log, drop extra features, and &lt;em&gt;degrade gracefully&lt;/em&gt; instead of crashing.”&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Fast feedback tied to change events
&lt;/h4&gt;

&lt;p&gt;When production starts returning loads of 5xxs right after a change, your first hypothesis should almost always be: &lt;em&gt;“We broke it.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Cloudflare initially suspected a large DDoS attack, partly because their off-platform status page also went down by coincidence, which complicated the diagnosis.&lt;/p&gt;

&lt;p&gt;A strong CD/operational posture ties monitoring directly to deployments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“We deployed change X to ClickHouse at 11:05.”&lt;/li&gt;
&lt;li&gt;“At 11:20, 5xxs spike globally.”&lt;/li&gt;
&lt;li&gt;The system should scream: &lt;em&gt;“Roll back that change first, then keep investigating.”&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To their credit, they did eventually correct course, stop propagation of the bad file, push out a good one, and restart the proxies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where CI/CD could have helped more
&lt;/h3&gt;

&lt;p&gt;Let’s talk about specific points where mature CI/CD practices can help avoid or limit this sort of outage.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Test the config generator like production code
&lt;/h4&gt;

&lt;p&gt;The change that triggered this was essentially a behaviour change in the query used to build the feature file.&lt;/p&gt;

&lt;p&gt;In a CD world, we’d treat that as an ordinary code change and ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do we have automated tests for the query behaviour?&lt;/li&gt;
&lt;li&gt;Do we have tests that assert the range of output sizes we consider safe?&lt;/li&gt;
&lt;li&gt;Do we have a test environment where a realistic ClickHouse cluster – including these permission changes – is exercised before we touch production?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A property test like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Given realistic metadata, the resulting feature file must have ≤ 200 features, or the build fails&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;...would have caught this before it ever reached live traffic.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Practice progressive delivery for everything
&lt;/h4&gt;

&lt;p&gt;Cloudflare already had a gradual rollout of permissions across the ClickHouse cluster, but the effect of that was system going up and down: sometimes a good config, sometimes a bad one.&lt;/p&gt;

&lt;p&gt;From a CD perspective, you want &lt;em&gt;controlled blast radius&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ship new behaviour to a tiny slice of traffic or a subset of regions first.&lt;/li&gt;
&lt;li&gt;Observe: “Did the new feature file cause any spike in 5xxs, latency, or resource usage?”&lt;/li&gt;
&lt;li&gt;Only then ramp up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of having the feature file immediately pushed to the entire network every five minutes, imagine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A canary group of edge nodes is updated first.&lt;/li&gt;
&lt;li&gt;If they see the bot module panic or error rates spike, an automated system:

&lt;ul&gt;
&lt;li&gt;Rolls back to the last known good config file.&lt;/li&gt;
&lt;li&gt;Blocks further rollout.&lt;/li&gt;
&lt;li&gt;Raises an incident with a clear “config rollout blocked” signal.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;That’s progressive delivery applied not just to code, but to ML feature sets and configuration.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Design for graceful degradation, not panic
&lt;/h4&gt;

&lt;p&gt;The Rust code in the affected FL2 proxy is essentially designed to "panic" (as per post-mortem) if it gets more than 200 features.&lt;/p&gt;

&lt;p&gt;From a resilience standpoint, that’s exactly what we &lt;em&gt;don’t&lt;/em&gt; want. In a world of continuous delivery and constant change, you assume your assumptions will be broken.&lt;/p&gt;

&lt;p&gt;Better options might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drop any features over the limit and log loudly.&lt;/li&gt;
&lt;li&gt;Disable the Bot Management module temporarily and continue forwarding traffic, maybe treating everything as “unknown bot score” rather than bringing down the proxy.&lt;/li&gt;
&lt;li&gt;Trip a feature kill-switch that turns off Bot Management globally while keeping the core CDN and proxy path alive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloudflare’s own remediation list mentions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hardening ingestion of internal configuration files like user input.&lt;/li&gt;
&lt;li&gt;More global kill switches for features.&lt;/li&gt;
&lt;li&gt;Reviewing error handling across core modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s &lt;em&gt;exactly&lt;/em&gt; the direction you’d expect from a team embracing CD and resilience engineering.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. CI as safety net for "platform-ish" changes
&lt;/h4&gt;

&lt;p&gt;One subtle thing here: this wasn’t a direct change to Bot Management. It was a change to how the &lt;em&gt;database platform&lt;/em&gt; handled permissions and metadata.&lt;/p&gt;

&lt;p&gt;In many organisations, platform changes are treated as “infra stuff”, not subject to the same product-level tests. But in a CD culture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Platform changes go through pipelines that &lt;em&gt;also&lt;/em&gt; run representative workloads and integration tests for the services that depend on them.&lt;/li&gt;
&lt;li&gt;That ClickHouse permission change should have run:

&lt;ul&gt;
&lt;li&gt;A compatibility test suite for all consumers that query &lt;code&gt;system.columns&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Specific tests for the Bot Management feature generator pipeline.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;If you can’t do that comprehensively, you at least start with the critical systems: anything that can bring down your main proxy path should have extremely strong automated protection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Good engineering is about turning failure into fast, safe learning
&lt;/h3&gt;

&lt;p&gt;Cloudflare did the right thing by publishing a detailed post-mortem, while expressing engineering humility and laying out concrete follow-up steps.&lt;/p&gt;

&lt;p&gt;Everyone running large-scale systems has failures. What distinguishes good engineering organizations is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They treat incidents as opportunities to improve the &lt;em&gt;system&lt;/em&gt;, not to blame individuals.&lt;/li&gt;
&lt;li&gt;They share what they learned.&lt;/li&gt;
&lt;li&gt;They make structural changes – to code, to pipelines, to architecture, to daily practices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From a continuous delivery perspective, the lessons I’d highlight are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every change is potentially dangerous&lt;/strong&gt;. CD is about making lots of small changes safe, not moving fast and ignoring risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config, queries, and ML features are code&lt;/strong&gt;. They need the same CI/CD discipline: tests, contracts, and progressive rollout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for graceful failure&lt;/strong&gt;. When – not if! – your assumptions are broken, the system should bend, not snap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tie observability tightly to deployments&lt;/strong&gt;. If you’ve just changed something and the world is on fire, suspect your change first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cloudflare's outage is painful for them and for a lot of the internet. But it’s also a rich example of why we do continuous delivery in the first place.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop calling bugs 'major' and 'minor'</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Mon, 15 Jul 2024 15:22:11 +0000</pubDate>
      <link>https://dev.to/markoa/stop-calling-bugs-major-and-minor-1he4</link>
      <guid>https://dev.to/markoa/stop-calling-bugs-major-and-minor-1he4</guid>
      <description>&lt;p&gt;I recently cleaned up &lt;a href="https://github.com/operately/operately/" rel="noopener noreferrer"&gt;Operately's&lt;/a&gt; issues (now all public). Split them into &lt;strong&gt;bugs&lt;/strong&gt; and &lt;strong&gt;papercuts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foqdj3eaubqy934xzusre.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foqdj3eaubqy934xzusre.jpg" alt="Current distribution of issue labels" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've seen many teams use "major", "minor", etc for bugs. But what does that really mean? We prefer more specific terms.&lt;/p&gt;

&lt;p&gt;There's a big difference between:&lt;/p&gt;

&lt;p&gt;❌ Something that should work but doesn't&lt;br&gt;
🚧 Something that's just unfinished or crude&lt;/p&gt;

&lt;p&gt;Calling every little issue a "bug" in a new product is demotivating.&lt;/p&gt;

&lt;p&gt;Hence, papercuts.&lt;/p&gt;

&lt;p&gt;Papercuts are small annoyances. Things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Usability quirks&lt;/li&gt;
&lt;li&gt;UI inconsistencies&lt;/li&gt;
&lt;li&gt;Any little thing that frustrates users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like, "&lt;em&gt;posting a comment on a goal update should generate a news feed item&lt;/em&gt;". Sure, it should. But we never implemented it. We will get to it, though.&lt;/p&gt;

&lt;p&gt;On their own, they seem trivial. But they add up fast.&lt;/p&gt;

&lt;p&gt;So of course, in theory you want to fix all your papercuts. Your product will feel smoother. Users will be happier.&lt;/p&gt;

&lt;p&gt;In reality, Operately is still in alpha stage and we have to balance feature completeness with a high enough quality to meet initial user expectations and validate it in the market.&lt;/p&gt;

&lt;p&gt;So for now the strategy is, we'll trim them to the level that we estimate is acceptable.&lt;/p&gt;

&lt;p&gt;If you read this this far 🫶 -- how do you handle bugs and papercuts in your projects?&lt;/p&gt;

</description>
      <category>development</category>
    </item>
    <item>
      <title>My Aha! Moment with Test-Driven Development</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Thu, 25 Nov 2021 13:19:27 +0000</pubDate>
      <link>https://dev.to/markoa/my-aha-moment-with-test-driven-development-137c</link>
      <guid>https://dev.to/markoa/my-aha-moment-with-test-driven-development-137c</guid>
      <description>&lt;p&gt;My aha! moment with test-driven development were &lt;strong&gt;acceptance tests&lt;/strong&gt; and I recommend everyone who is still a skeptic to start there.&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/semaphore" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1183%2Fc8d02a31-85ba-4528-b043-30ee19b6db2f.png" alt="Semaphore" width="500" height="500"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F246163%2Fd3e35cfd-7503-407d-a461-cccb8657c26f.jpg" alt="" width="800" height="599"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/semaphore/the-benefits-of-acceptance-testing-2b34" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;The Benefits of Acceptance Testing&lt;/h2&gt;
      &lt;h3&gt;Tomas Fernandez for Semaphore ・ Nov 18 '21&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#testing&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#beginners&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#cicd&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#programming&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;Many years ago I was working on a Ruby on Rails app with a multi-step booking process. Each step had many possible states and outcomes.&lt;/p&gt;

&lt;p&gt;Testing that manually after every change in code was painfully repetitive, slow, and embarrassingly unreliable. 😓&lt;/p&gt;

&lt;p&gt;Getting anything done was hard. We’d change one thing, then spend about an hour on manual testing in the web browser to check if everything is still working.&lt;/p&gt;

&lt;p&gt;We’d often be surprised by the fact that we broke a seemingly unrelated use case. 🤯&lt;/p&gt;

&lt;p&gt;Sometimes we’d figure that out on our own, and sometimes we’d ship a new version to production and the client would email us about what had just stopped working.&lt;/p&gt;

&lt;p&gt;Getting that kind of news felt the worst. It felt like we were wasting a lot of our client’s and our own time. 👎&lt;/p&gt;

&lt;p&gt;Then, we discovered &lt;a href="https://cucumber.io/" rel="noopener noreferrer"&gt;Cucumber&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://semaphoreci.com/community/tutorials/introduction-to-writing-acceptance-tests-with-cucumber" rel="noopener noreferrer"&gt;Very easy to get started&lt;/a&gt;. We could write a test once, and it would automatically launch a web browser, run the application, and do the work of feature verification instead of us.&lt;/p&gt;

&lt;p&gt;That felt like magic! 🪄 ✨&lt;/p&gt;

&lt;p&gt;As soon as we started writing acceptance tests for a feature we had been working on, our development accelerated by an order of magnitude. 🚀&lt;/p&gt;

&lt;p&gt;We could add and change code without worrying that we would break something. 😌&lt;/p&gt;

&lt;p&gt;When something did break, a failing scenario would let us know, and we’d quickly fix it before deploying a new version to users. 🦄&lt;/p&gt;

&lt;p&gt;So to all teams out there who don't write (much) tests, I Hundred points symbol recommend starting with acceptance tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ROI&lt;/strong&gt; — in peace of mind, reliability, velocity, and good feeling — of writing just a few is massive.&lt;/p&gt;

&lt;p&gt;From that point on, it's really a downhill ride to adopting a test-driven mindset for all layers of code. 💪&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published as a &lt;a href="https://twitter.com/markoa/status/1463855491814072324" rel="noopener noreferrer"&gt;Twitter thread&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Test Jupyter Notebooks with Pytest and Nbmake</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Thu, 08 Jul 2021 16:03:06 +0000</pubDate>
      <link>https://dev.to/semaphore/how-to-test-jupyter-notebooks-with-pytest-and-nbmake-fl3</link>
      <guid>https://dev.to/semaphore/how-to-test-jupyter-notebooks-with-pytest-and-nbmake-fl3</guid>
      <description>&lt;p&gt;&lt;a href="https://jupyter.org" rel="noopener noreferrer"&gt;Jupyter&lt;/a&gt; notebook files have been one of the &lt;a href="https://octoverse.github.com/2019/" rel="noopener noreferrer"&gt;fastest-growing&lt;/a&gt; content types on GitHub in recent years. They provide a simple interface for iterating on visual tasks, whether you are analyzing datasets or writing code-heavy documentation.&lt;/p&gt;

&lt;p&gt;Their popularity comes with problems though: large numbers of ipynb files accumulate in repos, many of which are in a broken state. As a result, it is difficult for people to re-run, or even understand your notebooks.&lt;/p&gt;

&lt;p&gt;This tutorial describes how you can use the pytest plugin &lt;a href="https://github.com/treebeardtech/nbmake" rel="noopener noreferrer"&gt;nbmake&lt;/a&gt; to automate end-to-end testing of notebooks.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Pre-requisites
&lt;/h2&gt;

&lt;p&gt;This guide builds on fundamental skills in testing Python projects which are described in &lt;em&gt;&lt;a href="https://semaphoreci.com/blog/python-continuous-integration-continuous-delivery" rel="noopener noreferrer"&gt;Python Continuous Integration and Deployment From Scratch&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Before proceeding, please ensure you have covered these basics and have your Python 3 toolchain of choice (such as pip + virtualenv) installed in your development environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Application
&lt;/h2&gt;

&lt;p&gt;It’s common for Python projects to contain a directory of notebook files (known by the .ipynb extension) which may contain any of the following contents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;proof-of-concept modeling code&lt;/li&gt;
&lt;li&gt;example API usage docs&lt;/li&gt;
&lt;li&gt;Lengthy scientific tutorials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the purpose of this tutorial, we are going to learn how to automate simple end-to-end tests on some notebooks containing Python exercises. Thanks to &lt;a href="https://github.com/norvig/pytudes" rel="noopener noreferrer"&gt;pytudes&lt;/a&gt; for providing this example material.&lt;/p&gt;

&lt;p&gt;Fork and clone the &lt;a href="https://github.com/treebeardtech/nbmake-examples" rel="noopener noreferrer"&gt;example project&lt;/a&gt; on GitHub.&lt;/p&gt;

&lt;p&gt;Inside this repo, you will find a directory &lt;code&gt;ipynb&lt;/code&gt; containing notebooks. Install the dependencies in the requirements.txt file, optionally creating yourself a virtual environment first.&lt;/p&gt;

&lt;p&gt;Before proceeding to the next step, see if you can run these notebooks in your editor of choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Notebooks Locally
&lt;/h2&gt;

&lt;p&gt;It’s likely that up to this point, your test process involves you manually running notebooks through the Jupyter Lab UI, or a similar client. This is both time-consuming and error-prone though.&lt;/p&gt;

&lt;p&gt;Let’s start automating this process in your development environment using nbmake as an initial step forward.&lt;/p&gt;

&lt;p&gt;Nbmake is a python package that serves as a pytest plugin for testing notebooks. It is developed by the author of this guide and is used by well-known scientific organizations such as Dask, Quansight, and Kitware. You can install it using pip, or a package manager of your choice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;nbmake&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you did not already have pytest installed, it will be installed for you as a dependency of nbmake.&lt;/p&gt;

&lt;p&gt;Before testing your notebooks for the first time, let’s check everything is set up correctly by instructing pytest to simply collect (but not run) all notebook test cases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ pytest &lt;span class="nt"&gt;--collect-only&lt;/span&gt; &lt;span class="nt"&gt;--nbmake&lt;/span&gt; &lt;span class="s2"&gt;"./ipynb"&lt;/span&gt; 
&lt;span class="o"&gt;================================&lt;/span&gt; &lt;span class="nb"&gt;test &lt;/span&gt;session starts &lt;span class="o"&gt;=================================&lt;/span&gt;
platform darwin &lt;span class="nt"&gt;--&lt;/span&gt; Python 3.8.2, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /Users/a/git/alex-treebeard/semaphore-demo-python-jupyter-notebooks
plugins: nbmake-0.5
collected 3 items                                                                    

&amp;lt;NotebookFile ipynb/Boggle.ipynb&amp;gt;
  &amp;lt;NotebookItem &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&amp;lt;NotebookFile ipynb/Cheryl-and-Eve.ipynb&amp;gt;
  &amp;lt;NotebookItem &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&amp;lt;NotebookFile ipynb/Differentiation.ipynb&amp;gt;
  &amp;lt;NotebookItem &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="o"&gt;=============================&lt;/span&gt; 3 tests collected &lt;span class="k"&gt;in &lt;/span&gt;0.01s &lt;span class="o"&gt;=============================&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, pytest has collected some notebook items using the nbmake plugin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you receive the message &lt;code&gt;unrecognized arguments: --nbmake&lt;/code&gt; then the nbmake plugin is not installed. This may happen if your CLI is invoking a pytest binary outside of your current virtual environment. Check where your pytest binary is located with &lt;code&gt;which pytest&lt;/code&gt; to confirm this.&lt;/p&gt;

&lt;p&gt;Now that we have validated that nbmake and pytest are working together and can see your notebooks, let’s run them for real.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ pytest &lt;span class="nt"&gt;--nbmake&lt;/span&gt; &lt;span class="s2"&gt;"./ipynb"&lt;/span&gt;
&lt;span class="o"&gt;================================&lt;/span&gt; &lt;span class="nb"&gt;test &lt;/span&gt;session starts &lt;span class="o"&gt;=================================&lt;/span&gt;
platform darwin &lt;span class="nt"&gt;--&lt;/span&gt; Python 3.8.2, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /Users/a/git/alex-treebeard/semaphore-demo-python-jupyter-notebooks
plugins: nbmake-0.5
collected 3 items                                                                    

ipynb/Boggle.ipynb &lt;span class="nb"&gt;.&lt;/span&gt;                                                           &lt;span class="o"&gt;[&lt;/span&gt; 33%]
ipynb/Cheryl-and-Eve.ipynb &lt;span class="nb"&gt;.&lt;/span&gt;                                                   &lt;span class="o"&gt;[&lt;/span&gt; 66%]
ipynb/Differentiation.ipynb &lt;span class="nb"&gt;.&lt;/span&gt;                                                  &lt;span class="o"&gt;[&lt;/span&gt;100%]

&lt;span class="o"&gt;=================================&lt;/span&gt; 3 passed &lt;span class="k"&gt;in &lt;/span&gt;37.65s &lt;span class="o"&gt;=================================&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great, they have passed. These are simple notebooks in a demo project though. It is very unlikely all of your notebooks will pass the first time in a few seconds, so let’s go through some approaches for getting complex projects running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed up Execution with pytest-xdist
&lt;/h3&gt;

&lt;p&gt;Large projects may have many notebooks, each taking a long time to install packages, pull data from the network, and perform analyses.&lt;/p&gt;

&lt;p&gt;If your tests are taking more than a few seconds, it’s worth checking how parallelizing the execution affects the runtime. We can do this with another pytest plugin &lt;code&gt;pytest-xdist&lt;/code&gt; as follows:&lt;/p&gt;

&lt;p&gt;First, install the xdist package. It is a pytest plugin similar to nbmake and will add new command-line options.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pytest-xdist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with the number of worker processes set to &lt;code&gt;auto&lt;/code&gt; using the following command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;--nbmake&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="s2"&gt;"./ipynb"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Ignore Expected Errors in a Notebook
&lt;/h3&gt;

&lt;p&gt;Not all notebooks will be easy to test automatically. Some will contain cells that need user input or will raise uncaught exceptions to illustrate some functionality.&lt;/p&gt;

&lt;p&gt;Fortunately, we can put directives in our notebook metadata to tell nbmake to ignore and continue after errors are thrown. You may not have used notebook metadata before, so it's a good time to mention that notebooks are just JSON files, despite their ipynb extension. You can add custom fields to extend their behavior.&lt;/p&gt;

&lt;p&gt;Your development environment may provide a UI for adding metadata to your notebook file, but let's do it in a raw text editor for educational purposes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your .ipynb file in a simple text editor such as &lt;a href="https://www.sublimetext.com/" rel="noopener noreferrer"&gt;Sublime&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Locate your &lt;code&gt;kernelspec&lt;/code&gt; field inside the notebook's metadata.&lt;/li&gt;
&lt;li&gt;Add an &lt;code&gt;execution&lt;/code&gt; object as a sibling to the &lt;code&gt;kernelspec&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This should leave you with something like this:&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;"cells"&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="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&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;"kernelspec"&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="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"execution"&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;"allow_errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&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;Now you can re-run the notebook to check that the JSON is valid and errors are ignored.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write Executed Notebooks Back to the Repo
&lt;/h3&gt;

&lt;p&gt;By default, tests are read-only: your ipynb file remains unchanged on disk. This is usually a good default because your editor may be open whilst you are running.&lt;/p&gt;

&lt;p&gt;In some situations however, you may want to persist the executed notebooks to disk, if you need to&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debug failing notebooks by viewing outputs&lt;/li&gt;
&lt;li&gt;Create a commit to your repository with notebooks in a reproducible state&lt;/li&gt;
&lt;li&gt;Build executed notebooks into a doc site, using nbsphinx or jupyter book&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can direct nbmake to persist the executed notebooks to disk using the &lt;code&gt;overwrite&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;--nbmake&lt;/span&gt; &lt;span class="nt"&gt;--overwrite&lt;/span&gt; &lt;span class="s2"&gt;"./ipynb"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Exclude Certain Notebooks from Test Runs
&lt;/h3&gt;

&lt;p&gt;Some notebooks may be difficult to test automatically, due to requiring user inputs via stdin or taking a long time to run.&lt;/p&gt;

&lt;p&gt;Use pytest’s ignore flag to de-select them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; pytest &lt;span class="nt"&gt;--nbmake&lt;/span&gt; docs &lt;span class="nt"&gt;--overwrite&lt;/span&gt; &lt;span class="nt"&gt;--ignore&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docs/landing-page.ipynb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; this will not work if you are selecting all notebooks using a glob pattern such as (&lt;code&gt;"*ipynb"&lt;/code&gt;) which manually overrides pytest's ignore flags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate Notebook Tests on Semaphore CI
&lt;/h2&gt;

&lt;p&gt;Using the techniques above, it is possible to create an automated testing process using Semaphore &lt;a href="https://semaphoreci.com/continuous-integration" rel="noopener noreferrer"&gt;continuous integration&lt;/a&gt; (CI). Pragmatism is key: finding a way to test most of your notebooks and ignoring difficult ones is a good first step to improving quality.&lt;/p&gt;

&lt;p&gt;Start by creating a project for the repo containing your notebooks. Select "Choose repository".&lt;/p&gt;

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

&lt;p&gt;Next, connect Semaphore to the repo containing your notebooks.&lt;/p&gt;

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

&lt;p&gt;Skip past "Add People" for now, so we can set up our workflow from scratch (even if you are using the demo repo).&lt;/p&gt;

&lt;p&gt;Semaphore gives us some starter configuration, we are going to &lt;strong&gt;customize&lt;/strong&gt; it first before running it.&lt;/p&gt;

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

&lt;p&gt;Create a simple single-block workflow with the following details:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For &lt;strong&gt;Name of the Block&lt;/strong&gt; we will use &lt;em&gt;Test&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;Name of the Job&lt;/strong&gt; we will use &lt;em&gt;Test Notebooks&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For &lt;strong&gt;Commands&lt;/strong&gt; we will use the following:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;checkout
cache restore
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
pip &lt;span class="nb"&gt;install &lt;/span&gt;nbmake pytest-xdist
cache store
pytest &lt;span class="nt"&gt;--nbmake&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto ./ipynb
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For &lt;strong&gt;Prologue&lt;/strong&gt; use the following to configure Python 3:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sem-version python 3.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now we can &lt;strong&gt;run the workflow&lt;/strong&gt;. If the workflow fails, don't worry, we'll address common problems next.&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Troubleshooting Some Common Errors
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Add Missing Jupyter Kernel to Your CI Environment
&lt;/h3&gt;

&lt;p&gt;If you are using a kernel name other than the default ‘python3’. You will see an error message when executing your notebooks in a fresh CI environment: &lt;code&gt;Error - No such kernel: 'mycustomkernel'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;ipykernel&lt;/code&gt; to install your custom kernel if you are using Python.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; ipykernel &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; mycustomkernel 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are using another language such as c++ in your notebooks, you may have a different process for installing your kernel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Missing Secrets to Your CI Environment
&lt;/h3&gt;

&lt;p&gt;In some cases, your notebook will fetch data from APIs requiring an API token or authenticated CLI. Anything that works on your development environment should work on Semaphore, so first check &lt;a href="https://docs.semaphoreci.com/guided-tour/environment-variables-and-secrets/" rel="noopener noreferrer"&gt;this post&lt;/a&gt; to see how to set up secrets.&lt;/p&gt;

&lt;p&gt;Once you have installed secrets into Semaphore, you may need to configure the notebook to read the secrets from an environment variable available in CI environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Missing Dependencies to Your CI Environment
&lt;/h3&gt;

&lt;p&gt;The python data science stack often requires native libraries to be installed. If you are using conda, it is likely that they will be covered in your normal installation process. It is slightly less likely if you are using standard python libraries.&lt;/p&gt;

&lt;p&gt;If you are struggling to install the libraries that you need, have a look at &lt;a href="https://docs.semaphoreci.com/ci-cd-environment/custom-ci-cd-environment-with-docker/" rel="noopener noreferrer"&gt;creating a CI docker image&lt;/a&gt;. It will be easier to test locally and more stable over time than using Semaphore’s default environments.&lt;/p&gt;

&lt;p&gt;Please remember the advice on pragmatism; you can often achieve 90% of the value in 10% of the time. This may involve tradeoffs like ignoring notebooks that run ML models requiring a GPU.&lt;/p&gt;

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

&lt;p&gt;This guide demonstrated how you can automate testing of Jupyter Notebooks so you can maintain their reproducibility.&lt;/p&gt;

&lt;p&gt;Notebooks have proven to be an effective tool for constructing technical narratives. Due to their novelty however, some software teams rule out their usage until clearer processes emerge to test, review, and reuse their contents.&lt;/p&gt;

&lt;p&gt;There is still some way to go before notebook technologies fit into software projects seamlessly but now is a good time to get a head start. You may want to investigate some of the following further work items to further improve your operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;pre-commit&lt;/a&gt; and &lt;a href="https://pypi.org/project/nbstripout/" rel="noopener noreferrer"&gt;nbstripout&lt;/a&gt; to remove bulky notebook output data before committing changes&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://github.com/executablebooks/jupyter-book" rel="noopener noreferrer"&gt;jupyter book&lt;/a&gt; to compile your notebooks into a beautiful documentation site&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://www.reviewnb.com" rel="noopener noreferrer"&gt;ReviewNB&lt;/a&gt; to review notebooks in pull requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading, and please share any tips for working with notebooks with us!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was contributed by &lt;a href="https://github.com/alex-treebeard" rel="noopener noreferrer"&gt;Alex Remedios&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>jupyter</category>
      <category>python</category>
      <category>testing</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Automated Testing: The Cornerstone of CI/CD</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Wed, 28 Apr 2021 11:10:56 +0000</pubDate>
      <link>https://dev.to/semaphore/automated-testing-the-cornerstone-of-ci-cd-9db</link>
      <guid>https://dev.to/semaphore/automated-testing-the-cornerstone-of-ci-cd-9db</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was contributed by &lt;a href="https://jesuswasrasta.com" rel="noopener noreferrer"&gt;Ferdinando Santacroce&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the Italian language, there is a single word for computer science: &lt;em&gt;Informatica&lt;/em&gt;. It is a portmanteau of “informazione automatica” and, as you may have already spotted, it means something like information automation.&lt;/p&gt;

&lt;p&gt;Automation is a pillar of information technology. Among experienced software professionals, it becomes instinctual. Every time you find yourself doing the same job over and over again, the desire to write a script and automate a repetitive task asserts itself.&lt;/p&gt;

&lt;p&gt;This attitude is key for software developers, but sometimes things get overcomplicated. Then, automation becomes difficult, and manual work becomes necessary. This article describes what automated testing is, the path to a fully automated test suite, and the challenges that lie along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is automated testing?
&lt;/h2&gt;

&lt;p&gt;Like any other material good, software needs to be tested before reaching the customer. The obvious way to test something is to use it for a while to make sure it behaves as expected. This is the way most developers in the past used to check their work before shipping it.&lt;/p&gt;

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

&lt;p&gt;After all, you have written it, you have it on your computer, you know how to run it, and you know how to quickly test it as you make changes.&lt;/p&gt;

&lt;p&gt;The flip side is that you as the creator are biased. You might forget to test some parts or might not realize the ramifications of a change. Also, you can’t be 100% sure that your code will work in an environment that isn’t your development machine.&lt;/p&gt;

&lt;p&gt;Even if you do everything right, testing is a boring task — a time-consuming activity where skilled, creative professionals are forced to do mindless drudgery. Historically, the countermeasure was hiring people to test, making developers happy and removing biases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpf5sfbi7yvagv3sdpw88.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpf5sfbi7yvagv3sdpw88.png" alt="manual software testing with qa people" width="788" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This seemingly win-win solution makes things worse more often than not. Developers lose the overall picture and awareness of all the moving parts when they aren’t involved in testing. Testers, on the other hand, spend an enormous amount of time just making things work and have to bother developers whenever they find something they don’t understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The role of a QA team when testing is automated
&lt;/h2&gt;

&lt;p&gt;Testers are part of what is known as the Quality Assurance (QA) team. They are paramount for shipping high-quality products, so it’s important to let them do their job efficiently. They do not have to test everything every time, nor catch unhandled exceptions or 404 errors. They are there to help developers and companies to deliver good quality software. Otherwise, the “QA is doing the testing anyway” mindset settles in, giving rise to dysfunctional behaviors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foljmh66j75m5xlss47bh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foljmh66j75m5xlss47bh.png" alt="qa with automated test suite" width="483" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The inflection point for automated testing
&lt;/h2&gt;

&lt;p&gt;In 1999, the book &lt;em&gt;&lt;a href="https://www.goodreads.com/book/show/67833.Extreme_Programming_Explained" rel="noopener noreferrer"&gt;Extreme Programming Explained&lt;/a&gt;&lt;/em&gt; laid the groundwork for a revolution, which later resulted in the &lt;em&gt;&lt;a href="https://agilemanifesto.org/iso/en/manifesto.html" rel="noopener noreferrer"&gt;Manifesto for Agile Software Development&lt;/a&gt;&lt;/em&gt;. In this text, we find the cornerstones of modern software development: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rapid feedback loops.&lt;/li&gt;
&lt;li&gt;Simple and safe ways to modify code. As Kent Beck said, “&lt;a href="https://twitter.com/kentbeck/status/250733358307500032" rel="noopener noreferrer"&gt;make the change easy, then make the easy change&lt;/a&gt;.”&lt;/li&gt;
&lt;li&gt;A happy and satisfied team when the work is done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this field, automated testing plays a key role. Let’s have a look at some kinds of tests we can put into use.&lt;/p&gt;

&lt;h2&gt;
  
  
  From unit tests to end-to-end tests
&lt;/h2&gt;

&lt;p&gt;The topic of testing is very broad and there are dozens of different types of tests, depending on the needs of the developer. Let’s suppose, for example, that we want to create software that can manage prices in a supermarket. The code below is an example of a &lt;strong&gt;unit test&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;When_I_add_an_apple_the_system_charges_50_cents&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;CashRegister&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CashRegister&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"apple"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

 &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;expectedCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
 &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;actualCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

 &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedCost&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actualCost&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this test, we are verifying that our cash register charges 50 cents for an apple. Although simplified, the example gives a good idea of what it means to test a “unit of code” (the cash register), in a very focused way. If the price of an apple changes or the system stops pricing apples as expected, this test will alert us. A unit test validates an assertion on a specific portion of code.&lt;/p&gt;

&lt;p&gt;There are other types of tests, aimed at ensuring the expected behavior of the application as a whole, from the graphical interface to the database. These kinds of tests are called &lt;strong&gt;acceptance and end-to-end (E2E) tests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Our range of tests, therefore, goes from unit tests, which are focused on the technical details, to acceptance tests, which instead show that the required business objectives are being met.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kd"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Supermarket Bundles        
           To increase the revenues as a Supermarket Manager, 
           I want to apply discounts and offers to my customers

&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; if customer buys 3 apples, he pays $1.00
    &lt;span class="nf"&gt;Given &lt;/span&gt;the apples costs 50 cents each
    &lt;span class="nf"&gt;Given &lt;/span&gt;the “buy 3 apples, pay 2” promotion 
    &lt;span class="nf"&gt;When &lt;/span&gt;the cashier scans 3 apples
    &lt;span class="nf"&gt;Then &lt;/span&gt;the cash register charges $1.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the excerpt above, we see an example of a specification written in Gherkin, a language made famous by &lt;a href="https://cucumber.io/" rel="noopener noreferrer"&gt;Cucumber&lt;/a&gt; and used in &lt;a href="https://semaphoreci.com/community/tutorials/behavior-driven-development" rel="noopener noreferrer"&gt;Behavior Driven Development (BDD)&lt;/a&gt;. Despite appearing as natural language, there are automated tests hidden below the statements in this scenario.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The characteristics of a good test
&lt;/h2&gt;

&lt;p&gt;All good tests have some features in common.&lt;/p&gt;

&lt;h3&gt;
  
  
  A good test is deterministic
&lt;/h3&gt;

&lt;p&gt;A good test is deterministic and idempotent. It doesn’t matter where and when it runs, it should always produce the same (verifiable) outputs given the same inputs. Nothing is more frustrating than an unreliable test that fails randomly due to unknown or uncontrollable conditions.&lt;/p&gt;

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

&lt;p&gt;Writing deterministic tests is not always easy, especially if the codebase was not designed to be testable from the beginning. Good programmers should take all the time they need to write deterministic tests. &lt;/p&gt;

&lt;h3&gt;
  
  
  A good test is fully-automated
&lt;/h3&gt;

&lt;p&gt;A good test must be endlessly and effortlessly repeatable. As a developer works to implement new features, an external server can work to ensure the expected behavior. This is achieved by continuously running all the tests without any additional effort.&lt;/p&gt;

&lt;p&gt;Machines are good at repetitive tasks, so let them do what they were designed for. Leave the developers with the higher and more rewarding task of satisfying users by unleashing their ingenuity.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  A good test is responsive
&lt;/h3&gt;

&lt;p&gt;A good test provides quick, honest feedback. Experienced developers know that testing is primarily about feedback. Having a test suite that offers feedback after hours or days is a big problem because it breaks &lt;a href="https://semaphoreci.com/blog/2016/11/03/how-bdd-and-continuous-delivery-help-developers-maintain-flow.html" rel="noopener noreferrer"&gt;cognitive flow&lt;/a&gt;. A developer can more quickly and effectively fix bugs the sooner they are discovered.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Build a robust test suite
&lt;/h3&gt;

&lt;p&gt;A solid test suite has tests separated according to how long they take, their complexity, and the resources available. Unit tests run very fast — usually a matter of milliseconds — while end-to-end tests are slower, and more often than not they require staging environments to be properly installed.&lt;/p&gt;

&lt;p&gt;In the end, a good developer should build a well-organized test suite over time, and write plenty of unit tests to run continuously. Here’s where &lt;strong&gt;a CI/CD pipeline&lt;/strong&gt; comes into play.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The heart of Continuous Integration (CI)
&lt;/h2&gt;

&lt;p&gt;At regular intervals, developers integrate changes with those of the other team members, using a versioning tool such as Git. A well-established team has a strong &lt;a href="https://semaphoreci.com/community/tutorials/continuous-integration" rel="noopener noreferrer"&gt;Continuous Integration policy&lt;/a&gt; and a &lt;a href="https://semaphoreci.com/continuous-integration" rel="noopener noreferrer"&gt;CI pipeline&lt;/a&gt; that, upon noticing the changes, will take the new code from the repository and run tests on it — from quick unit tests to more rigorous acceptance tests. More costly tests can be run less frequently, such as once or twice a day. This lets the developers know immediately, with a good degree of confidence, if something stops working.&lt;/p&gt;

&lt;p&gt;As in many aspects of life, there are trade-offs in testing strategy. It’s up to the developers and testers to negotiate the best compromise, ensuring quality and efficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simplicity and automation are prerequisites for safety
&lt;/h2&gt;

&lt;p&gt;As mentioned earlier, software professionals must ensure the quality of their own work. Quality means low defectiveness, and low defectiveness means a vast amount of testing. Thus, writing good tests and building a system that can execute them quickly and accurately isn’t optional. Teams must commit to building a safety net with &lt;a href="https://semaphoreci.com/cicd" rel="noopener noreferrer"&gt;CI/CD&lt;/a&gt; to ensure smooth development.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Myths of Test Automation
&lt;/h2&gt;

&lt;p&gt;Despite the growing awareness achieved by companies and software developers in recent years, test automation is still plagued by some myths.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing slows down development
&lt;/h3&gt;

&lt;p&gt;Those who claim that testing slows down development are both wrong and right. They’re right because in some cases, especially in TDD, writing tests makes developers take the time to think ahead about the result they want to achieve, setting aside the frenzy of writing code for a moment. In this case, slowing down is an act of will, a precise means of focusing on the problem ahead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73mmf1r3li6egvzqasr0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73mmf1r3li6egvzqasr0.png" alt="red green refactor tdd cycle" width="419" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the long run, however, those who think that writing tests are a waste of time are wrong. We must think of the time spent writing tests as an investment because a failure-safe environment makes it possible to make changes and experiments at a very high speed. Developers can come to a solution in a fraction of the time it would take if they constantly had to be careful about where they put their feet. &lt;/p&gt;

&lt;p&gt;It’s like walking home on the sidewalk versus walking on a tightrope stretched between two buildings: tightrope walkers are not famous for their speed.&lt;/p&gt;

&lt;p&gt;Abraham Lincoln used to say “&lt;em&gt;Give me six hours to chop down a tree and I will spend the first four sharpening the axe&lt;/em&gt;“. Testing automation is like having a little helper that constantly sharpens your axe, so you can “hack” down any problems that pop up without spending hours sharpening it yourself first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing is only for finding bugs
&lt;/h3&gt;

&lt;p&gt;Another myth is that testing is only for finding bugs. Going after bugs is one possible purpose, but certainly not the only one. The real advantage lies in making bugs easily detectable and fixable after every single change. That is what test automation is all about: making it easier for a developer to alter code because they know they have a safety net. &lt;/p&gt;

&lt;h3&gt;
  
  
  You must achieve 100% coverage
&lt;/h3&gt;

&lt;p&gt;Modern development environments allow you to calculate a metric called &lt;strong&gt;code coverage&lt;/strong&gt;. It represents the amount of code covered by tests. It is quite natural for the non-test-savvy to think that 100% coverage guarantees the total absence of bugs in their codebase. This belief, apart from being incorrect, is a harbinger of bad practices.&lt;/p&gt;

&lt;p&gt;Coverage only guarantees that there is at least one test that puts a certain portion of code under scrutiny. It does not guarantee that said test makes sense, that it verifies all the possible combinations of inputs, or that it will always behave as expected. &lt;/p&gt;

&lt;p&gt;Writing tests is a profession somewhere between art and science, and coverage is certainly not the tool that guarantees the quality of a test suite. Furthermore, the drive for 100% coverage leads developers to test trivial portions of code, wasting time and money, and feeding an unhealthy perception of procedural infallibility. Setting a more reasonable threshold, such as 80%, might be a good compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to define a good testing strategy?
&lt;/h2&gt;

&lt;p&gt;At this point, the question arises: what are the steps to creating a good test suite? Let’s try to define a roadmap for a company or a team that wants to change course and start testing its codebase.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Start testing
&lt;/h3&gt;

&lt;p&gt;No, this is not a joke: the first thing you need to do is &lt;a href="https://semaphoreci.com/blog/continuous-integration-fast-fundamental-tests-first" rel="noopener noreferrer"&gt;start testing&lt;/a&gt;. Usually a simple query on a search engine like “ testing tools” provides plenty of hints and tutorials to get started. &lt;/p&gt;

&lt;p&gt;But even if you’re using outdated or abstruse languages, you can always find a way to test your product. If there are no frameworks or ready-made tools on the market, you can always write tests using pure programming languages or with bare shell scripts. &lt;/p&gt;

&lt;p&gt;To paraphrase Walt Disney, “&lt;em&gt;If you can run it, you can test it&lt;/em&gt;.“&lt;/p&gt;

&lt;h3&gt;
  
  
  Identify a meaningful part, and write an automated test
&lt;/h3&gt;

&lt;p&gt;In codebases that have developed with no inclination to testability, the road is uphill at first.&lt;/p&gt;

&lt;p&gt;The first tests written should offer a good balance between value and affordability, i.e. code that is sufficiently easy to approach, but that at the same time can provide you with some value. Testing a part that never breaks, a trivial feature, or one that almost nobody uses is not helpful. Moreover, going straight to the main feature of your product usually ends with brittle tests and frustrated developers.&lt;/p&gt;

&lt;p&gt;Most of the time, we can start by writing some end-to-end tests that try to emulate the behavior of the user. If we have a web application, for example, we can begin by training a browser with tools like &lt;a href="https://www.selenium.dev/documentation/en/" rel="noopener noreferrer"&gt;Selenium&lt;/a&gt; or &lt;a href="https://www.cypress.io/" rel="noopener noreferrer"&gt;Cypress&lt;/a&gt;. Let’s not, however, limit ourselves to tools; we also have to be ready to manually write our test routines, shell scripts, and whatever else is needed to make the test automatic. &lt;/p&gt;

&lt;p&gt;In the journey we take, we always discover useful details for increasing our understanding of the codebase, details that will be valuable when we go to modify or refactor bits of code. Once we understand the basic principles of testing, we can start learning about testing frameworks, mocking libraries, and all the tools that make a developer’s job easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create your safety net
&lt;/h3&gt;

&lt;p&gt;A good test suite is a safety net. In the beginning, when you don’t have tests, every change is an unknown risk. Over time, and with the addition of tests, the risk decreases, confidence increases, and development accelerates. &lt;/p&gt;

&lt;p&gt;It’s a long process. You can’t hope to solve all the accumulated problems in a few weeks. The important thing is to keep adding tests, and reinforcing testing practices in the team.&lt;/p&gt;

&lt;p&gt;Make automation an integral part from the beginning. If a new project is about to take off, don’t waste the opportunity to start off with the right foot with a well-rounded testing strategy and a solid set of CI tools. Once the right procedure for conducting a test has been identified, the test must be automated, and then it must be run in a secure, isolated environment: a CI/CD pipeline. &lt;/p&gt;

&lt;p&gt;This is about feedback: the longer you wait, the later you will know if your automated tests are as isolated and idempotent as you want them to be, or if they “just work on your computer”.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>testing</category>
    </item>
    <item>
      <title>Monorepo Support Is Now Generally Available on Semaphore</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Wed, 07 Apr 2021 16:18:47 +0000</pubDate>
      <link>https://dev.to/semaphore/monorepo-support-is-now-generally-available-on-semaphore-2bm8</link>
      <guid>https://dev.to/semaphore/monorepo-support-is-now-generally-available-on-semaphore-2bm8</guid>
      <description>&lt;p&gt;Our mission at &lt;a href="https://semaphoreci.com" rel="noopener noreferrer"&gt;Semaphore&lt;/a&gt; is to empower engineers to ship great products by providing them with a state-of-the-art &lt;a href="https://semaphoreci.com/cicd" rel="noopener noreferrer"&gt;CI/CD experience&lt;/a&gt;. We’re excited to announce that Semaphore now provides out-of-the-box support for monorepo projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is monorepo? 🚝
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://semaphoreci.com/blog/what-is-monorepo" rel="noopener noreferrer"&gt;monorepo&lt;/a&gt; is a version-controlled code repository that holds many projects. While these may be related, they are often logically independent and run by different teams.&lt;/p&gt;

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


&lt;div class="ltag__link"&gt;
  &lt;a href="/semaphore" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1183%2Fc8d02a31-85ba-4528-b043-30ee19b6db2f.png" alt="Semaphore" width="500" height="500"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F246163%2Fd3e35cfd-7503-407d-a461-cccb8657c26f.jpg" alt="" width="800" height="599"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/semaphore/what-is-monorepo-and-should-you-use-it-5454" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;What is monorepo? (and should you use it)&lt;/h2&gt;
      &lt;h3&gt;Tomas Fernandez for Semaphore ・ Mar 31 '21&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How it works? 🤔
&lt;/h2&gt;

&lt;p&gt;The key building block of monorepo CI/CD pipelines on Semaphore is the &lt;a href="https://docs.semaphoreci.com/reference/conditions-reference/#change_in" rel="noopener noreferrer"&gt;&lt;code&gt;change_in&lt;/code&gt; function&lt;/a&gt; which detects if a file or set of files have changed in a particular Git commit range.&lt;/p&gt;

&lt;p&gt;You can use the change_in function to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run custom workflows for changes on specific files&lt;/strong&gt;. Use &lt;code&gt;change_in&lt;/code&gt; in combination with glob patterns to define jobs that will run only when certain files change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip unnecessary checks&lt;/strong&gt;. No need to run that whole test suite when you change &lt;code&gt;README.md&lt;/code&gt;. Save some CI time by using exclude option to define files and folders you want to ignore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuously deploy the right things&lt;/strong&gt;. Combine &lt;code&gt;change_in&lt;/code&gt; with &lt;a href="https://docs.semaphoreci.com/reference/pipeline-yaml-reference/#auto_promote" rel="noopener noreferrer"&gt;automatic promotions&lt;/a&gt; to deploy what has changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08x3gbob4r71twd273p0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08x3gbob4r71twd273p0.png" alt="Monorepo CI/CD workflow in action" width="800" height="1093"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So what? 😜
&lt;/h2&gt;

&lt;p&gt;Skipping parts of your code which were not affected by a change can lead to a dramatically faster feedback loop.&lt;/p&gt;

&lt;p&gt;For example, the front-end engineering team at BlueLabs was able to &lt;a href="https://semaphoreci.com/customers/bluelabs" rel="noopener noreferrer"&gt;reduce their build time from 17mins to 4mins&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fel8mdsl61q40muke5mjs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fel8mdsl61q40muke5mjs.png" alt="BlueLabs monorepo ci/cd workflow" width="638" height="705"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How can I use this? 😄
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://semaphoreci.com" rel="noopener noreferrer"&gt;Semaphore&lt;/a&gt; is free for personal, hobby and open source projects, so giving this a spin is pretty easy.&lt;/p&gt;

&lt;p&gt;You can fork a &lt;a href="https://github.com/semaphoreci-demos/semaphore-demo-monorepo" rel="noopener noreferrer"&gt;pre-made monorepo demo project&lt;/a&gt; and run it yourself.&lt;/p&gt;

&lt;p&gt;Or you can just add your own project and play with it.&lt;/p&gt;

&lt;p&gt;Happy building!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;A more PR-ish version of this announcement was originally published on the &lt;a href="https://semaphoreci.com/blog/monorepo-support-available" rel="noopener noreferrer"&gt;Semaphore blog&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

</description>
      <category>news</category>
      <category>devops</category>
      <category>monorepo</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Talking CI/CD on Changelog’s Go Time Podcast</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Wed, 10 Feb 2021 14:49:25 +0000</pubDate>
      <link>https://dev.to/markoa/talking-ci-cd-on-changelog-s-go-time-podcast-228j</link>
      <guid>https://dev.to/markoa/talking-ci-cd-on-changelog-s-go-time-podcast-228j</guid>
      <description>&lt;div class="podcastliquidtag"&gt;
  &lt;div class="podcastliquidtag__info"&gt;
    &lt;a href="/gotime/we-re-talkin-ci-cd"&gt;
      &lt;h1 class="podcastliquidtag__info__episodetitle"&gt;We're talkin' CI/CD&lt;/h1&gt;
    &lt;/a&gt;
    &lt;a href="/gotime"&gt;
      &lt;h2 class="podcastliquidtag__info__podcasttitle"&gt;
        Go Time
      &lt;/h2&gt;
    &lt;/a&gt;
  &lt;/div&gt;
  &lt;div id="record-we-re-talkin-ci-cd" class="podcastliquidtag__record"&gt;
    &lt;img class="button play-butt" id="play-butt-we-re-talkin-ci-cd" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fplaybutt-5e444a2eae28832efea0dec3342ccf28a228b326c47f46700d771801f75d6b88.png" alt="play"&gt;
    &lt;img class="button pause-butt" id="pause-butt-we-re-talkin-ci-cd" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fpausebutt-bba7cb5f432cfb16510e78835378fa22f45fa6ae52a624f7c9794fefa765c384.png" alt="pause"&gt;
    &lt;img class="podcastliquidtag__podcastimage" id="podcastimage-we-re-talkin-ci-cd" alt="Go Time" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fpodcast%2Fimage%2F5%2Fgotime_800x800.png"&gt;
  &lt;/div&gt;

  &lt;div class="hidden-audio" id="hidden-audio-we-re-talkin-ci-cd"&gt;
  
    
    Your browser does not support the audio element.
  
  &lt;div id="progressBar" class="audio-player-display"&gt;
    &lt;a href="/gotime/we-re-talkin-ci-cd"&gt;
      &lt;img id="episode-profile-image" alt="We're talkin' CI/CD" width="420" height="420" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fpodcast%2Fimage%2F5%2Fgotime_800x800.png"&gt;
      &lt;img id="animated-bars" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fanimated-bars-4e8c57c8b58285fcf7d123680ad8af034cd5cd43b4d9209fe3aab49d1e9d77b3.gif" alt="animated volume bars"&gt;
    &lt;/a&gt;
    &lt;span id="barPlayPause"&gt;
      &lt;img class="butt play-butt" alt="play" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fplaybutt-5e444a2eae28832efea0dec3342ccf28a228b326c47f46700d771801f75d6b88.png"&gt;
      &lt;img class="butt pause-butt" alt="pause" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fpausebutt-bba7cb5f432cfb16510e78835378fa22f45fa6ae52a624f7c9794fefa765c384.png"&gt;
    &lt;/span&gt;
    &lt;span id="volume"&gt;
      &lt;span id="volumeindicator" class="volume-icon-wrapper showing"&gt;
        &lt;span id="volbutt"&gt;
          &lt;img alt="volume" class="icon-img" height="16" width="16" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fvolume-cd20707230ae3fc117b02de53c72af742cf7d666007e16e12f7ac11ebd8130a7.png"&gt;
        &lt;/span&gt;
        &lt;span class="range-wrapper"&gt;
          
        &lt;/span&gt;
      &lt;/span&gt;
      &lt;span id="mutebutt" class="volume-icon-wrapper hidden"&gt;
        &lt;img alt="volume-mute" class="icon-img" height="16" width="16" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fvolume-mute-8f08ec668105565af8f8394eb18ab63acb386adbe0703afe3748eca8f2ecbf3b.png"&gt;
      &lt;/span&gt;
      &lt;span class="speed" id="speed"&gt;1x&lt;/span&gt;
    &lt;/span&gt;
    &lt;span class="buffer-wrapper" id="bufferwrapper"&gt;
      &lt;span id="buffer"&gt;&lt;/span&gt;
      &lt;span id="progress"&gt;&lt;/span&gt;
      &lt;span id="time"&gt;initializing...&lt;/span&gt;
      &lt;span id="closebutt"&gt;×&lt;/span&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;I had an honor to speak on &lt;a href="https://changelog.com/" rel="noopener noreferrer"&gt;Changelog’s&lt;/a&gt; &lt;a href="https://changelog.com/gotime/" rel="noopener noreferrer"&gt;Go Time podcast&lt;/a&gt; along with &lt;a href="https://twitter.com/jpetazzo" rel="noopener noreferrer"&gt;Jérôme Petazzoni&lt;/a&gt; on &lt;a href="https://semaphoreci.com/cicd" rel="noopener noreferrer"&gt;Continuous Integration and Delivery (CI/CD)&lt;/a&gt;. We discussed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What problems does CI/CD solve?&lt;/li&gt;
&lt;li&gt;What does it look like when it’s done well?&lt;/li&gt;
&lt;li&gt;What are the pitfalls in doing CI/CD and how to avoid them?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🎙 &lt;a href="https://changelog.com/gotime/162" rel="noopener noreferrer"&gt;Listen and/or read the transcript here — I hope you enjoy the show&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Android Continuous Integration and Deployment Tutorial</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Tue, 10 Nov 2020 13:25:42 +0000</pubDate>
      <link>https://dev.to/semaphore/android-continuous-integration-and-deployment-tutorial-g7a</link>
      <guid>https://dev.to/semaphore/android-continuous-integration-and-deployment-tutorial-g7a</guid>
      <description>&lt;p&gt;As we work on an Android app, we test our code regularly — hopefully with the help of automated tests. And when it's time to release a new version, we assemble the source code into an APK or AppBundle format which we deploy into the Google Play Store.&lt;/p&gt;

&lt;p&gt;It seems like a simple process, but there are several other steps involved, like signing the package and updating the version code and name.&lt;/p&gt;

&lt;p&gt;After a while, going through this process manually gets very tedious and tiring, so how can we automate this process?&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Semaphore
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://semaphoreci.com/" rel="noopener noreferrer"&gt;Semaphore&lt;/a&gt; is the fastest &lt;a href="https://dev.to/markoa/continuous-integration-explained-59f9"&gt;continuous integration (CI)&lt;/a&gt; and &lt;a href="https://dev.to/semaphore/ci-cd-continuous-integration-delivery-explained-75l"&gt;continuous delivery (CD)&lt;/a&gt; service which helps you automate your workflow by building, testing, and deploying your Android project. This way you can focus on what matters: &lt;strong&gt;your code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this tutorial we will explain how to set up Semaphore for a native Android project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;If you wish to follow this tutorial you'll need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/book/en/v2/Getting-Started-Installing-Git" rel="noopener noreferrer"&gt;Git&lt;/a&gt; and a
&lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; account.&lt;/li&gt;
&lt;li&gt;A Semaphore account. You can get one for free at
&lt;a href="https://semaphoreci.com/" rel="noopener noreferrer"&gt;semaphoreci.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://play.google.com/apps/publish" rel="noopener noreferrer"&gt;Google Play Developer account&lt;/a&gt; to
publish the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to finish this tutorial in 1 minute
&lt;/h2&gt;

&lt;p&gt;To get started &lt;em&gt;really&lt;/em&gt; quickly, do this:&lt;/p&gt;

&lt;p&gt;In Semaphore, follow the link in the top navigation to &lt;strong&gt;create a new project&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;From the examples list, &lt;strong&gt;select Android (Kotlin)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Semaphore will fork a demo project:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/semaphoreci-demos" rel="noopener noreferrer"&gt;
        semaphoreci-demos
      &lt;/a&gt; / &lt;a href="https://github.com/semaphoreci-demos/semaphore-demo-android" rel="noopener noreferrer"&gt;
        semaphore-demo-android
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;And set up a complete CI/CD pipeline for you in less than a minute:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fckuqrgngfza3t0nldc5u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fckuqrgngfza3t0nldc5u.png" alt="The Android CI/CD pipeline on Semaphore" width="800" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s it. You’re done. *&lt;em&gt;mic drop&lt;/em&gt;*&lt;/p&gt;

&lt;p&gt;The demo is a simple app that lists all the open source &lt;a href="https://github.com/semaphoreci-demos/" rel="noopener noreferrer"&gt;Semaphore demo projects&lt;/a&gt; on GitHub. It includes unit and integration tests, and has the &lt;a href="https://github.com/Triple-T/gradle-play-publisher/" rel="noopener noreferrer"&gt;Gradle Play Publisher&lt;/a&gt; plugin set up that adds a Gradle task to publish your app to the store.&lt;/p&gt;

&lt;p&gt;You can see this app live &lt;a href="https://play.google.com/store/apps/details?id=com.semaphoreci.demo" rel="noopener noreferrer"&gt;on the Play Store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, I know that you’re most likely here to learn how to set up continuous integration for your own Android project. So let's see how to do that using the demo project as a foundation.&lt;/p&gt;

&lt;p&gt;Adapting these instructions to your project should not be too difficult. But if you do run into a challenge, feel free to ask a question in the comments section. 💬&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring CI/CD on Semaphore from scratch
&lt;/h2&gt;

&lt;p&gt;After creating your Semaphore account you'll need to &lt;a href="https://docs.semaphoreci.com/guided-tour/creating-your-first-project/" rel="noopener noreferrer"&gt;create your first Semaphore project&lt;/a&gt;. Your Semaphore configuration is stored in a directory at the root of your repository called &lt;code&gt;.semaphore&lt;/code&gt;. The entry point of your &lt;a href="https://dev.to/markoa/ci-cd-pipeline-a-gentle-introduction-2n8k"&gt;CI/CD pipeline&lt;/a&gt; is defined by the &lt;code&gt;semaphore.yml&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;In this tutorial we will explain how to configure an Android CI/CD workflow by reading the configuration files. That way you’ll learn the most about Semaphore and Android continuous integration in general.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;you don't have to write YAML by hand&lt;/strong&gt;. You can also use Semaphore’s &lt;a href="https://semaphoreci.com/blog/cicd-workflow-builder" rel="noopener noreferrer"&gt;graphical workflow builder&lt;/a&gt; for a more visual approach.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👉 &lt;a href="https://semaphore-demos.semaphoreci.com/workflows/a89a9d45-3bbe-4edb-a84a-08748a17d61b?pipeline_id=e33b9a63-a85e-4875-be01-65e4b4bf7cde" rel="noopener noreferrer"&gt;See this public workflow&lt;/a&gt; to get a taste of Semaphore’s UI and the final project running.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Continuous integration: the safety net for your Android project
&lt;/h2&gt;

&lt;p&gt;We want to perform regular checks on the code by running a linting tool and by running the project tests, and we want those to be automated. Let's check on what can we do on Semaphore and edit the &lt;code&gt;semaphore.yml&lt;/code&gt; configuration file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👉 You can see the full file of the demo project &lt;a href="https://github.com/semaphoreci-demos/semaphore-demo-android/blob/dev/.semaphore/semaphore.yml" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Agent: where your code runs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;machine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;e1-standard-2&lt;/span&gt;
    &lt;span class="na"&gt;os_image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu1804&lt;/span&gt;
  &lt;span class="na"&gt;containers&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;main&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;registry.semaphoreci.com/android:29&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we're defining the environment where the build will run. We want a Linux machine with 2 vCPUs, 4 GB of RAM that runs Ubuntu 18.04 OS. We also want to run our jobs in a container that has the latest (at the time of writing) Android SDK installed, provided by &lt;a href="https://docs.semaphoreci.com/ci-cd-environment/semaphore-registry-images/" rel="noopener noreferrer"&gt;Semaphore Container Registry&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Semaphore always caches the &lt;a href="https://docs.semaphoreci.com/ci-cd-environment/android-images/" rel="noopener noreferrer"&gt;two latest stable versions of Android SDK&lt;/a&gt; to ensure that the machine is ready in no time, making our builds faster.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Global job configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global_job_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;env_vars&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;ADB_INSTALL_TIMEOUT&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10"&lt;/span&gt;
  &lt;span class="na"&gt;secrets&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;android_keys&lt;/span&gt;
  &lt;span class="na"&gt;prologue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mv ~/release-keystore.jks ~/$SEMAPHORE_GIT_DIR/app/release-keystore.jks&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cache restore gradle-wrapper&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cache restore gradle-cache&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cache restore android-build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Semaphore's &lt;a href="https://docs.semaphoreci.com/reference/pipeline-yaml-reference/#global_job_config" rel="noopener noreferrer"&gt;Global Job Config&lt;/a&gt; allows us to define a set of configurations that are shared across the whole pipeline. This is very helpful since it allows us to define everything in one&lt;br&gt;
place instead of repeating it in every task.&lt;/p&gt;

&lt;p&gt;Here we're defining a new environment variable &lt;code&gt;ADB_INSTALL_TIMEOUT&lt;/code&gt; so that&lt;br&gt;
&lt;code&gt;adb&lt;/code&gt; doesn't timeout while we're setting up the emulator and running the&lt;br&gt;
integration tests.&lt;/p&gt;

&lt;p&gt;We're also saying that we want to use the &lt;a href="https://docs.semaphoreci.com/essentials/using-secrets/" rel="noopener noreferrer"&gt;global secrets&lt;/a&gt; &lt;code&gt;android_keys&lt;/code&gt; that will allow us to use our keys and passwords defined as secrets, as environment variables in the build.&lt;/p&gt;

&lt;p&gt;Specifically on the &lt;a href="https://github.com/semaphoreci-demos/semaphore-demo-android" rel="noopener noreferrer"&gt;demo project&lt;/a&gt; we're saving the following secrets on &lt;code&gt;android_keys&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;release-keystore.jks&lt;/code&gt;: the keystore file used to sign our release builds;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RELEASE_KEYSTORE_PASSWORD&lt;/code&gt;: our keystore password;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RELEASE_KEY_ALIAS&lt;/code&gt;: our keystore key alias;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RELEASE_KEY_PASSWORD&lt;/code&gt;: our keystore key password;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service-account-key.json&lt;/code&gt;: the &lt;a href="(https://github.com/Triple-T/gradle-play-publisher/#service-account)"&gt;service account
key&lt;/a&gt;,
used to deploy the demo app to the &lt;a href="https://play.google.com/store/apps/details?id=com.semaphoreci.demo" rel="noopener noreferrer"&gt;Play
Store&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the ideal place so save sensitive data, such as keys. Be sure to use &lt;a href="https://docs.semaphoreci.com/essentials/using-secrets/" rel="noopener noreferrer"&gt;secrets&lt;/a&gt; on your builds instead of pushing sensitive information into your git repository.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.semaphoreci.com/reference/pipeline-yaml-reference/#prologue-and-epilogue" rel="noopener noreferrer"&gt;prologue&lt;/a&gt; section allow us to run a set of commands before the job begins. We're checking out the code from the git repository, moving our keystore file to the correct path and restoring our cache so builds are faster.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pipeline blocks
&lt;/h3&gt;

&lt;p&gt;Here's where we can add all the build tasks we want to run. We define the jobs&lt;br&gt;
of each task, and the commands we want to execute in this pipeline. Let's start&lt;br&gt;
at the top.&lt;/p&gt;
&lt;h4&gt;
  
  
  Build Block
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build'&lt;/span&gt;
  &lt;span class="na"&gt;task&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="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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Project'&lt;/span&gt;
        &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gradlew bundle&lt;/span&gt;
    &lt;span class="na"&gt;epilogue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;on_pass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cache clear&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cache store gradle-wrapper ~/.gradle/wrapper&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cache store gradle-cache ~/.gradle/caches&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cache store android-build ~/.android/build-cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The first thing we want to do is to build our project by running &lt;code&gt;./gradlew&lt;br&gt;
bundle&lt;/code&gt; to check that everything is ok. Then, in the &lt;a href="https://docs.semaphoreci.com/reference/pipeline-yaml-reference/#prologue-and-epilogue" rel="noopener noreferrer"&gt;epilogue&lt;/a&gt;, that will only run if this job is successful, we are refreshing the cache.&lt;/p&gt;
&lt;h4&gt;
  
  
  Verification Block
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Verification'&lt;/span&gt;
  &lt;span class="na"&gt;skip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pull_request&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!~&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'.*'"&lt;/span&gt;
  &lt;span class="na"&gt;task&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="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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Analyze&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Code'&lt;/span&gt;
        &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gradlew lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Unit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Tests'&lt;/span&gt;
        &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gradlew 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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Integration&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Tests'&lt;/span&gt;
        &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# Install the required tools and the emulator itself&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sdkmanager "platform-tools" "platforms;android-29" "build-tools;30.0.0" "emulator"&lt;/span&gt;
          &lt;span class="c1"&gt;# Install system images for the emulator&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sdkmanager "system-images;android-29;google_apis;x86"&lt;/span&gt;
          &lt;span class="c1"&gt;# Create an emulator with the installed system images&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo no | avdmanager create avd -n test-emulator -k "system-images;android-29;google_apis;x86"&lt;/span&gt;
          &lt;span class="c1"&gt;# Start the emulator with no audio, boot animation, window, and with GPU acceleration off&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;emulator -avd test-emulator -noaudio -no-boot-anim -gpu off -no-window &amp;amp;&lt;/span&gt;
          &lt;span class="c1"&gt;# Wait for the emulator to boot completely&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;'&lt;/span&gt;
          &lt;span class="c1"&gt;# Dismiss the emulator lock screen and wait 1 second for it to settle&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;adb shell wm dismiss-keyguard&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
          &lt;span class="c1"&gt;# Disable window and transition animations. This is required to run UI tests correctly&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;adb shell settings put global window_animation_scale &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;adb shell settings put global transition_animation_scale &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;adb shell settings put global animator_duration_scale &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gradlew connectedAndroidTest&lt;/span&gt;
    &lt;span class="na"&gt;epilogue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;always&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;artifact push job --expire-in 2w --destination reports/ app/build/reports/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now the real work begins. This is the verification block where we're doing a&lt;br&gt;
lint check and running our tests. We also have a &lt;a href="https://docs.semaphoreci.com/reference/pipeline-yaml-reference/#skip-in-blocks" rel="noopener noreferrer"&gt;skip condition&lt;/a&gt; that tells Semaphore that we only want to run this block when a pull request is&lt;br&gt;
open.&lt;/p&gt;

&lt;p&gt;Here's what's happening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;./gradlew lint&lt;/code&gt; runs the &lt;a href="https://developer.android.com/studio/write/lint" rel="noopener noreferrer"&gt;Android
Lint&lt;/a&gt; that analyzes the code
to check if there's any problems.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;./gradlew test&lt;/code&gt; will run all the &lt;a href="https://developer.android.com/studio/test/command-line" rel="noopener noreferrer"&gt;Unit
Tests&lt;/a&gt; on our project.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;./gradlew connectedAndroidTest&lt;/code&gt; will run the &lt;a href="https://developer.android.com/studio/test/command-line" rel="noopener noreferrer"&gt;Integration
Tests&lt;/a&gt;, after setting
up the emulator.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Usually, this is enough to verify the code and should always be checked/ran before merging and deploying a new app version so that we're sure that we're not introducing bugs or bad code into the project. You can add more &lt;a href="https://docs.semaphoreci.com/reference/pipeline-yaml-reference/#jobs" rel="noopener noreferrer"&gt;jobs&lt;/a&gt; here if you need to perform additional checks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After everything completes, on the epilogue, we're uploading our test reports as &lt;a href="https://docs.semaphoreci.com/essentials/artifacts/" rel="noopener noreferrer"&gt;artifacts&lt;/a&gt; so if something goes wrong with our tests we can consult the reports to see more details, making our lives a bit easier.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👉 It's important to note that every thing will run in parallel making Semaphore builds extremely fast.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Promotions
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.semaphoreci.com/guided-tour/deploying-with-promotions/" rel="noopener noreferrer"&gt;Promotions&lt;/a&gt; are used when we want to branch out our pipeline when certain conditions are met. This is &lt;strong&gt;perfect for deployments&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;promotions&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;Internal Deploy&lt;/span&gt;
    &lt;span class="na"&gt;pipeline_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-internal.yml&lt;/span&gt;
    &lt;span class="na"&gt;auto_promote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'passed'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AND&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'dev'"&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;Beta Deploy&lt;/span&gt;
    &lt;span class="na"&gt;pipeline_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-beta.yml&lt;/span&gt;
    &lt;span class="na"&gt;auto_promote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'passed'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AND&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'master'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have two other pipelines, and like so two promotions. We want to promote to the pipeline that deploys our app to the internal track when we push new code to &lt;code&gt;dev&lt;/code&gt;, and to the beta track when we push new code to &lt;code&gt;master&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F92rhsqpgbll3azfxknnz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F92rhsqpgbll3azfxknnz.png" alt="Verification pipeline" width="756" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the pipeline in it's full glory. Notice how quick it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous Deployment: you can do it with Android too!
&lt;/h2&gt;

&lt;p&gt;After verifying that everything is ok with our project we want to automate deploys.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Check the full configurations for the &lt;a href="https://github.com/semaphoreci-demos/semaphore-demo-android/blob/dev/.semaphore/deploy-internal.yml" rel="noopener noreferrer"&gt;internal pipeline&lt;/a&gt; and &lt;a href="https://github.com/semaphoreci-demos/semaphore-demo-android/blob/dev/.semaphore/deploy-beta.yml" rel="noopener noreferrer"&gt;beta pipeline&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most of the configuration is the same as explained before. We only added a command to the global config to move the service account key to the correct path as that is needed to deploy the app and added a new block.&lt;/p&gt;

&lt;h4&gt;
  
  
  Deployment Block
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Internal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Deployment'&lt;/span&gt;
  &lt;span class="na"&gt;task&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="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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deploy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Play&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Store&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Beta&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Track"&lt;/span&gt;
        &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gradlew publishReleaseBundle --track beta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we're simply running the publish task that was created by the &lt;a href="https://github.com/Triple-T/gradle-play-publisher" rel="noopener noreferrer"&gt;Google Play Publisher&lt;/a&gt; gradle plugin. The only thing that differs from the internal pipeline is the argument &lt;code&gt;track&lt;/code&gt; that specifies that we want to deploy to the &lt;code&gt;internal&lt;/code&gt; track instead. Also, we don't have to worry about incrementing the &lt;code&gt;versionCode&lt;/code&gt; every time because we're using the Semaphore workflow number &lt;a href="https://docs.semaphoreci.com/ci-cd-environment/environment-variables/" rel="noopener noreferrer"&gt;environment variable&lt;/a&gt; for that, that is always incremented when new a new build is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  On to you
&lt;/h2&gt;

&lt;p&gt;Remember to check the complete demo project on &lt;a href="https://github.com/semaphoreci-demos/semaphore-demo-android" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; for a complete example and be sure to explore the code on your own. Feel free to fork it and use it as the base of your next &lt;a href="https://semaphoreci.com/" rel="noopener noreferrer"&gt;Semaphore&lt;/a&gt; project.&lt;/p&gt;

&lt;p&gt;There are still somethings that you can do to improve the workflow, like adding &lt;a href="https://docs.semaphoreci.com/essentials/slack-notifications/" rel="noopener noreferrer"&gt;Slack notifications&lt;/a&gt;, using &lt;a href="https://docs.semaphoreci.com/essentials/webhook-notifications/" rel="noopener noreferrer"&gt;webhooks&lt;/a&gt;, updating a CHANGELOG file or even creating GitHub releases, but the nice thing about Semaphore, and CI/CD tools in general, is that now you can finally focus on what matters: the code for your next Android app.&lt;/p&gt;

&lt;p&gt;Have questions about this tutorial? Want to show off your results? Leave a comment below. 🙇‍♂️&lt;/p&gt;

</description>
      <category>android</category>
      <category>tutorial</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Talking CI/CD and containers on the Docker Live Show</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Tue, 13 Oct 2020 10:24:57 +0000</pubDate>
      <link>https://dev.to/markoa/talking-ci-cd-and-containers-on-the-docker-live-show-2goh</link>
      <guid>https://dev.to/markoa/talking-ci-cd-and-containers-on-the-docker-live-show-2goh</guid>
      <description>&lt;p&gt;I'd like to share with you a video of me being a guest at &lt;a href="https://www.youtube.com/c/BretFisherDockerandDevOps" rel="noopener noreferrer"&gt;Bret Fisher's Docker Live show&lt;/a&gt; which I think was a great discussion.&lt;/p&gt;

&lt;p&gt;The questions were pouring in from the start so the entire episode had a great flow.&lt;/p&gt;

&lt;p&gt;Some of the things we've discussed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The origins of Semaphore&lt;/li&gt;
&lt;li&gt;Git &amp;amp; GitHub flow&lt;/li&gt;
&lt;li&gt;The &lt;em&gt;&lt;a href="https://dev.to/semaphore/new-ebook-ci-cd-with-docker-and-kubernetes-4op0"&gt;CI/CD with Docker and Kubernetes&lt;/a&gt;&lt;/em&gt; book&lt;/li&gt;
&lt;li&gt;The importance of automated tests and how to get started&lt;/li&gt;
&lt;li&gt;How to set up and optimize &lt;a href="https://dev.to/markoa/ci-cd-pipeline-a-gentle-introduction-2n8k"&gt;CI/CD pipelines&lt;/a&gt; when using Docker&lt;/li&gt;
&lt;li&gt;Semaphore's Workflow Builder as a way of making CI/CD more accessible to developers&lt;/li&gt;
&lt;li&gt;...and more!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch it — and let me know if you have any questions or feedback:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/1cL4c4edkFM"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>cicd</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>When Does It Make Sense to Use Docker?</title>
      <dc:creator>Marko Anastasov</dc:creator>
      <pubDate>Wed, 30 Sep 2020 14:46:14 +0000</pubDate>
      <link>https://dev.to/markoa/when-does-it-make-sense-to-use-docker-4pa8</link>
      <guid>https://dev.to/markoa/when-does-it-make-sense-to-use-docker-4pa8</guid>
      <description>&lt;p&gt;Using Docker / containers makes sense only if you need to standardize &lt;a href="https://dev.to/markoa/ci-cd-pipeline-a-gentle-introduction-2n8k"&gt;deployment pipelines&lt;/a&gt;. Otherwise it’s a waste of time.&lt;/p&gt;

&lt;p&gt;When do you need to standardize anything? When you're dealing with a a large quantity.&lt;/p&gt;

&lt;p&gt;So, either you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Have many (as in, double digits) microservices comprising one system, or&lt;/li&gt;
&lt;li&gt;Are in a &lt;em&gt;very&lt;/em&gt; large organization with many development teams.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So if you have one or two apps running on Heroku / Beanstalk — leave them there. You’re not missing out on anything. It's pure overhead.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
