<?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: Shanawaz Mohammed</title>
    <description>The latest articles on DEV Community by Shanawaz Mohammed (@shanawaz_mohammed_d4a9ace).</description>
    <link>https://dev.to/shanawaz_mohammed_d4a9ace</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4164588%2Ff81cb404-6577-4794-9b43-4a8d8708aaad.png</url>
      <title>DEV Community: Shanawaz Mohammed</title>
      <link>https://dev.to/shanawaz_mohammed_d4a9ace</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shanawaz_mohammed_d4a9ace"/>
    <language>en</language>
    <item>
      <title>How to Add Quality Gates to a GitHub Actions Pipeline (Step by Step)</title>
      <dc:creator>Shanawaz Mohammed</dc:creator>
      <pubDate>Mon, 05 Oct 2026 17:58:19 +0000</pubDate>
      <link>https://dev.to/shanawaz_mohammed_d4a9ace/how-to-add-quality-gates-to-a-github-actions-pipeline-step-by-step-1e5a</link>
      <guid>https://dev.to/shanawaz_mohammed_d4a9ace/how-to-add-quality-gates-to-a-github-actions-pipeline-step-by-step-1e5a</guid>
      <description>&lt;p&gt;Most CI pipelines run tests. Far fewer pipelines actually &lt;em&gt;stop&lt;/em&gt; bad code from reaching production. The difference is a &lt;strong&gt;quality gate&lt;/strong&gt;: a check that must pass before the pipeline is allowed to move to the next stage.&lt;/p&gt;

&lt;p&gt;In this tutorial, you'll build a GitHub Actions pipeline with four gates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lint:&lt;/strong&gt; code style and obvious errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests + coverage threshold:&lt;/strong&gt; fail if coverage drops below a minimum&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API tests:&lt;/strong&gt; verify the running service behaves correctly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual approval:&lt;/strong&gt; a human signs off before production&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the end, a failed gate will block deployment automatically.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A GitHub repository with a small Python web service (the same ideas apply to any language)&lt;/li&gt;
&lt;li&gt;Basic familiarity with YAML&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pytest&lt;/code&gt; and &lt;code&gt;pytest-cov&lt;/code&gt; in your &lt;code&gt;requirements.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the workflow file
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/pipeline.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/pipeline.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI/CD Pipeline&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install ruff&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruff check .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This first job is your cheapest gate. Linting takes seconds, so it should run first and fail fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add unit tests with a coverage threshold
&lt;/h2&gt;

&lt;p&gt;Add a second job. The key line is &lt;code&gt;--cov-fail-under=80&lt;/code&gt;, which makes pytest exit with an error if coverage drops below 80%.&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;unit-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lint&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest tests/unit --cov=app --cov-fail-under=80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;needs: lint&lt;/code&gt; creates the gate: this job only starts if linting passed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Don't start with an aggressive threshold on a legacy codebase. Measure your current coverage, set the threshold slightly below it, and raise it over time. A gate that always fails gets disabled.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: Add API tests against the running service
&lt;/h2&gt;

&lt;p&gt;Unit tests check functions in isolation. API tests check that the service actually works when it runs. Start the app in the background, wait for it to be healthy, then run the tests:&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;api-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unit-tests&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
      &lt;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;Start service&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;uvicorn app.main:app --port 8000 &amp;amp;&lt;/span&gt;
          &lt;span class="s"&gt;for i in $(seq 1 30); do&lt;/span&gt;
            &lt;span class="s"&gt;curl -sf http://localhost:8000/health &amp;amp;&amp;amp; break&lt;/span&gt;
            &lt;span class="s"&gt;sleep 1&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest tests/api --base-url http://localhost:8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The health-check loop matters. Without it, tests often start before the server is ready, which is one of the most common causes of flaky pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Require manual approval before production
&lt;/h2&gt;

&lt;p&gt;GitHub &lt;strong&gt;environments&lt;/strong&gt; let you require a reviewer before a job runs. In your repository, go to &lt;strong&gt;Settings → Environments → New environment&lt;/strong&gt;, name it &lt;code&gt;production&lt;/code&gt;, and add yourself under &lt;strong&gt;Required reviewers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then reference it in a deploy job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-tests&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main'&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./scripts/deploy.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the pipeline pauses after the API tests pass and waits for an approval in the GitHub UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Protect the main branch
&lt;/h2&gt;

&lt;p&gt;Gates only work if nobody can bypass them. Under &lt;strong&gt;Settings → Branches&lt;/strong&gt;, add a branch protection rule for &lt;code&gt;main&lt;/code&gt; and enable &lt;strong&gt;Require status checks to pass before merging&lt;/strong&gt;. Select &lt;code&gt;lint&lt;/code&gt;, &lt;code&gt;unit-tests&lt;/code&gt; and &lt;code&gt;api-tests&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Pull requests can no longer be merged while any gate is failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;th&gt;Typical runtime&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lint&lt;/td&gt;
&lt;td&gt;Style issues, unused imports, syntax errors&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit tests + coverage&lt;/td&gt;
&lt;td&gt;Logic bugs, untested code&lt;/td&gt;
&lt;td&gt;1–3 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API tests&lt;/td&gt;
&lt;td&gt;Integration and contract failures&lt;/td&gt;
&lt;td&gt;2–5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual approval&lt;/td&gt;
&lt;td&gt;Business or timing risk&lt;/td&gt;
&lt;td&gt;Human decision&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ordering gates from cheapest to most expensive keeps feedback fast: most problems are caught in the first minute, and expensive checks only run on code that is already in reasonable shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run API tests in parallel with a matrix strategy as the suite grows.&lt;/li&gt;
&lt;li&gt;Publish test reports as artifacts so failures are easy to debug.&lt;/li&gt;
&lt;li&gt;Add a security scanning gate (for example, dependency scanning) before deploy.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>testing</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Audit-Ready CI/CD: What Regulated Industries Teach Us About Shipping Software</title>
      <dc:creator>Shanawaz Mohammed</dc:creator>
      <pubDate>Mon, 05 Oct 2026 17:57:29 +0000</pubDate>
      <link>https://dev.to/shanawaz_mohammed_d4a9ace/audit-ready-cicd-what-regulated-industries-teach-us-about-shipping-software-258b</link>
      <guid>https://dev.to/shanawaz_mohammed_d4a9ace/audit-ready-cicd-what-regulated-industries-teach-us-about-shipping-software-258b</guid>
      <description>&lt;p&gt;In many startups, a release is a merge and a deploy button. In a regulated industry like banking, every release must also answer a set of questions, often months later, from an auditor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who&lt;/strong&gt; approved this change?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt; exactly was deployed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was it tested&lt;/strong&gt;, and where is the evidence?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Could anyone have bypassed&lt;/strong&gt; the process?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For years, many teams answered these questions with spreadsheets, screenshots and change-request forms filled in by hand. That approach is slow, error-prone and painful for engineers.&lt;/p&gt;

&lt;p&gt;My view, after years of working on QA and DevOps in a regulated environment: &lt;strong&gt;a well-designed CI/CD pipeline is the best compliance tool you have.&lt;/strong&gt; And the practices that make a pipeline audit-ready make it better for every team, regulated or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 1: The pipeline is the only road to production
&lt;/h2&gt;

&lt;p&gt;If engineers can deploy from their laptops, no amount of documentation will satisfy an auditor, or protect you from mistakes.&lt;/p&gt;

&lt;p&gt;In practice, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Production credentials live only in the CI/CD system, never on developer machines.&lt;/li&gt;
&lt;li&gt;Branch protection prevents direct pushes to the main branch.&lt;/li&gt;
&lt;li&gt;Every production change, including configuration and infrastructure, goes through the same pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the pipeline is the only road, its logs become a complete history of what changed in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 2: Separation of duties, enforced by tooling
&lt;/h2&gt;

&lt;p&gt;A classic control in regulated environments is that the person who writes a change should not be the only person who approves it.&lt;/p&gt;

&lt;p&gt;Modern tooling makes this easy to enforce rather than just document:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Require at least one reviewer on pull requests, and prevent authors from approving their own.&lt;/li&gt;
&lt;li&gt;Use protected deployment environments with required approvers for production.&lt;/li&gt;
&lt;li&gt;Restrict who can change the pipeline definitions themselves, because whoever controls the pipeline controls the controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Principle 3: Evidence is generated, not collected
&lt;/h2&gt;

&lt;p&gt;The biggest shift is moving from &lt;em&gt;collecting&lt;/em&gt; evidence after the fact to &lt;em&gt;generating&lt;/em&gt; it automatically during every run.&lt;/p&gt;

&lt;p&gt;For each release, the pipeline can produce and store:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linked ticket or change request&lt;/td&gt;
&lt;td&gt;Commit message or PR metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer and approver identities&lt;/td&gt;
&lt;td&gt;Pull request and environment approvals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test results and coverage&lt;/td&gt;
&lt;td&gt;JUnit/coverage reports stored as artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security scan results&lt;/td&gt;
&lt;td&gt;Dependency and container scan reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact artifact deployed&lt;/td&gt;
&lt;td&gt;Image digest or build checksum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment time and target&lt;/td&gt;
&lt;td&gt;Pipeline logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When an auditor asks about a release, you export a record instead of reconstructing history from memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 4: Immutable, traceable artifacts
&lt;/h2&gt;

&lt;p&gt;"We deployed version 2.3" is not good enough. Was it built from the same commit that was tested? Was it rebuilt between environments?&lt;/p&gt;

&lt;p&gt;Build once, then promote the &lt;em&gt;same&lt;/em&gt; artifact through test, staging and production, identified by a checksum or image digest rather than a mutable tag like &lt;code&gt;latest&lt;/code&gt;. That gives you a clear chain: this commit → this build → these tests → this deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 5: Quality gates are controls
&lt;/h2&gt;

&lt;p&gt;Automated tests, coverage thresholds and security scans are not just engineering hygiene. In a regulated context, they &lt;em&gt;are&lt;/em&gt; the controls. When a gate is defined in code, versioned and enforced on every run, it is far more reliable than a manual checklist.&lt;/p&gt;

&lt;p&gt;The flip side: any gate that can be skipped needs its own control. If there is an emergency bypass, it should require extra approval and be logged and reviewed afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters outside banking
&lt;/h2&gt;

&lt;p&gt;You don't need a regulator to benefit from these practices. Teams that adopt them get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster incident investigation ("what changed?" has an immediate answer)&lt;/li&gt;
&lt;li&gt;Safer releases through consistent, enforced checks&lt;/li&gt;
&lt;li&gt;Easier onboarding, because the process lives in code rather than tribal knowledge&lt;/li&gt;
&lt;li&gt;A head start if a customer or partner ever asks for SOC 2 or ISO 27001 evidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compliance is often seen as the enemy of speed. In my experience, the opposite is true: automating compliance into the pipeline removes manual work, and lets teams ship &lt;em&gt;more&lt;/em&gt; often with &lt;em&gt;more&lt;/em&gt; confidence.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>security</category>
    </item>
    <item>
      <title>6 Practical Ways to Fix Flaky Tests in Your CI Pipeline</title>
      <dc:creator>Shanawaz Mohammed</dc:creator>
      <pubDate>Mon, 05 Oct 2026 17:56:56 +0000</pubDate>
      <link>https://dev.to/shanawaz_mohammed_d4a9ace/6-practical-ways-to-fix-flaky-tests-in-your-ci-pipeline-5d69</link>
      <guid>https://dev.to/shanawaz_mohammed_d4a9ace/6-practical-ways-to-fix-flaky-tests-in-your-ci-pipeline-5d69</guid>
      <description>&lt;p&gt;A flaky test passes sometimes and fails sometimes, with no change to the code. One flaky test is an annoyance. Twenty of them destroy trust in your pipeline: developers start clicking "re-run" by reflex, and real failures slip through because everyone assumes it's "just flakiness again."&lt;/p&gt;

&lt;p&gt;After many years working on QA and release pipelines, I've found that most flaky tests come from a small number of root causes. Here are six practical fixes, roughly in the order you should try them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Measure before you fix
&lt;/h2&gt;

&lt;p&gt;You can't fix what you can't see. Before changing any tests, find out &lt;em&gt;which&lt;/em&gt; tests are flaky and &lt;em&gt;how often&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A simple approach: re-run your suite several times on the same commit and record which tests produce different results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run the suite 10 times and keep each JUnit report&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 10&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;pytest tests/ &lt;span class="nt"&gt;--junitxml&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/run-&lt;span class="nv"&gt;$i&lt;/span&gt;.xml &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any test that both passed and failed across those runs is flaky. Many CI platforms and test-reporting tools can also track this automatically over time. Rank the flaky tests by failure rate and start with the worst offenders.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Replace fixed sleeps with explicit waits
&lt;/h2&gt;

&lt;p&gt;This is the single most common cause I see, especially in UI and API tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Flaky: assumes the page loads within 2 seconds
&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;submit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a busy CI runner, two seconds is sometimes not enough. Wait for a &lt;em&gt;condition&lt;/em&gt; instead of a &lt;em&gt;duration&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Stable: waits up to 10 seconds, but continues as soon as the button is clickable
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.support.ui&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebDriverWait&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.support&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;expected_conditions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;EC&lt;/span&gt;

&lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WebDriverWait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;EC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;element_to_be_clickable&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;submit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same principle applies to services: poll a health endpoint until it responds rather than sleeping for a fixed time after startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Make tests independent of each other
&lt;/h2&gt;

&lt;p&gt;If test B only passes when test A runs first, you have hidden shared state. It usually shows up when tests run in parallel or in a different order.&lt;/p&gt;

&lt;p&gt;Common culprits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests that share a database row or user account&lt;/li&gt;
&lt;li&gt;Global variables or singletons modified by one test&lt;/li&gt;
&lt;li&gt;Files written to a shared temporary folder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; each test creates its own data and cleans it up. In pytest, fixtures are the natural tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;
    &lt;span class="n"&gt;api_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick way to expose order dependencies is to run your suite in random order (for example with the &lt;code&gt;pytest-randomly&lt;/code&gt; plugin) and see what breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Control time, randomness and external services
&lt;/h2&gt;

&lt;p&gt;Tests that depend on things outside your control will eventually fail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; a test that checks "created today" can fail when run near midnight. Freeze or inject the clock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Randomness:&lt;/strong&gt; seed random generators so failures are reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External APIs:&lt;/strong&gt; a third-party sandbox that is slow or down will fail your build. Mock it in unit tests, and keep a small number of real integration tests in a separate stage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Quarantine, don't ignore
&lt;/h2&gt;

&lt;p&gt;When a flaky test can't be fixed immediately, move it into a &lt;strong&gt;quarantine&lt;/strong&gt; group that still runs but does not block the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.quarantine&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_payment_callback_timeout&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Blocking stage: everything except quarantined tests&lt;/span&gt;
pytest &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"not quarantine"&lt;/span&gt;

&lt;span class="c"&gt;# Non-blocking stage: quarantined tests, results reported but not enforced&lt;/span&gt;
pytest &lt;span class="nt"&gt;-m&lt;/span&gt; quarantine &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules make quarantine work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every quarantined test has an owner and a ticket.&lt;/li&gt;
&lt;li&gt;The quarantine list is reviewed regularly. Quarantine is a waiting room, not a graveyard.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  6. Treat automatic retries as a last resort
&lt;/h2&gt;

&lt;p&gt;Retry plugins can make the pipeline green again quickly, but they hide the problem. If you do use retries, limit them (one retry at most), log every retry, and track retried tests as flaky so they still get fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;Fixing flakiness is unglamorous work, but the return is large. A pipeline that developers trust gets faster feedback, fewer "re-run and hope" cycles, and real failures that get investigated instead of ignored.&lt;/p&gt;

&lt;p&gt;Start small: measure this week, fix your top five flaky tests next week, and add a quarantine process so new flakiness never quietly piles up again.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
