<?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: David Bernard</title>
    <description>The latest articles on DEV Community by David Bernard (@davidb31).</description>
    <link>https://dev.to/davidb31</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%2F586753%2Fa7cda031-9b0c-4b24-9850-26e4fd554e9c.jpeg</url>
      <title>DEV Community: David Bernard</title>
      <link>https://dev.to/davidb31</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/davidb31"/>
    <language>en</language>
    <item>
      <title>CDEvents in Action #7: Instrument Any CI Step in a Few Lines</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Mon, 13 Apr 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/davidb31/cdevents-in-action-7-instrument-any-ci-step-in-a-few-lines-3k7l</link>
      <guid>https://dev.to/davidb31/cdevents-in-action-7-instrument-any-ci-step-in-a-few-lines-3k7l</guid>
      <description>&lt;p&gt;&lt;em&gt;Webhook integrations (&lt;a href="https://cdviz.dev/blog/20251007-episode-3-cicd-integration" rel="noopener noreferrer"&gt;ep#3&lt;/a&gt;, &lt;a href="https://cdviz.dev/blog/20251020-episode-4-webhook-transformers" rel="noopener noreferrer"&gt;ep#4&lt;/a&gt;) tell you when a pipeline started and whether it passed. They don't tell you which test suites ran, which failed, or how long each step took. &lt;code&gt;send --run&lt;/code&gt; fills that gap.&lt;/em&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%2F42pxdwr7khbzxytncjwk.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%2F42pxdwr7khbzxytncjwk.jpg" alt="sample testsuite dashboard" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;Prefix any CI command with &lt;code&gt;cdviz-collector send --run &amp;lt;type&amp;gt; [options] --&lt;/code&gt;:&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;# before&lt;/span&gt;
npm &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c"&gt;# after — expects npm test to produce JUnit XML output&lt;/span&gt;
cdviz-collector send &lt;span class="nt"&gt;--run&lt;/span&gt; testsuiterun_junit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CDVIZ_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$CDVIZ_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The collector runs your command normally. When it exits, it emits &lt;code&gt;testSuiteRun.started&lt;/code&gt; + &lt;code&gt;testSuiteRun.finished&lt;/code&gt; CDEvents with parsed test results included. Your exit code is preserved — CI still fails if tests fail.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br&gt;
Authentication: &lt;code&gt;--header "Authorization: Bearer $TOKEN"&lt;/code&gt; is shown here for clarity. HMAC signature authentication is also supported and may be preferred depending on your server configuration. See the &lt;a href="https://cdviz.dev/docs/cdviz-collector/send" rel="noopener noreferrer"&gt;send documentation&lt;/a&gt; for options.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Choose the Right &lt;code&gt;--run&lt;/code&gt; Type
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;--run&lt;/code&gt; value&lt;/th&gt;
&lt;th&gt;CDEvents emitted&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;testsuiterun_junit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;testSuiteRun.started&lt;/code&gt; / &lt;code&gt;.finished&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Tests that output JUnit XML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;testsuiterun_tap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;testSuiteRun.started&lt;/code&gt; / &lt;code&gt;.finished&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Tests that output TAP format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;testsuiterun_sarif&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;testSuiteRun.started&lt;/code&gt; / &lt;code&gt;.finished&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Security/lint scans (SARIF output)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;taskrun&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;taskRun.started&lt;/code&gt; / &lt;code&gt;.finished&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Any other step (build, deploy, …)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
More options, custom run types, and output path overrides are available — see the &lt;a href="https://cdviz.dev/docs/cdviz-collector/send-run" rel="noopener noreferrer"&gt;send --run reference&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  CI Auto-Detection
&lt;/h2&gt;

&lt;p&gt;Branch, commit SHA, and job name are read automatically — &lt;strong&gt;no &lt;code&gt;--metadata&lt;/code&gt; flags needed for those&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CI system&lt;/th&gt;
&lt;th&gt;Variables detected automatically&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GITHUB_REF_NAME&lt;/code&gt;, &lt;code&gt;GITHUB_SHA&lt;/code&gt;, &lt;code&gt;GITHUB_JOB&lt;/code&gt;, &lt;code&gt;GITHUB_RUN_ID&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab CI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CI_COMMIT_REF_NAME&lt;/code&gt;, &lt;code&gt;CI_COMMIT_SHA&lt;/code&gt;, &lt;code&gt;CI_JOB_NAME&lt;/code&gt;, &lt;code&gt;CI_PIPELINE_ID&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jenkins&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;JENKINS_URL&lt;/code&gt;, &lt;code&gt;JOB_BASE_NAME&lt;/code&gt;, &lt;code&gt;BUILD_NUMBER&lt;/code&gt;, &lt;code&gt;GIT_BRANCH&lt;/code&gt;, &lt;code&gt;GIT_COMMIT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  GitHub Actions Example
&lt;/h2&gt;

&lt;p&gt;Install the binary once, then wrap each step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install cdviz-collector&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;curl --proto '=https' --tlsv1.2 -LsSf https://github.com/cdviz-dev/cdviz-collector/releases/latest/download/cdviz-collector-x86_64-unknown-linux-musl.tar.xz \&lt;/span&gt;
      &lt;span class="s"&gt;| tar xJ -C /usr/local/bin --strip-components=1 cdviz-collector-x86_64-unknown-linux-musl/cdviz-collector&lt;/span&gt;
    &lt;span class="s"&gt;chmod +x /usr/local/bin/cdviz-collector&lt;/span&gt;


&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;cdviz-collector send --run taskrun \&lt;/span&gt;
      &lt;span class="s"&gt;--url "${{ secrets.CDVIZ_URL }}" \&lt;/span&gt;
      &lt;span class="s"&gt;--header "Authorization: Bearer ${{ secrets.CDVIZ_TOKEN }}" \&lt;/span&gt;
      &lt;span class="s"&gt;-- npm run build&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test&lt;/span&gt; &lt;span class="c1"&gt;# expects JUnit XML output from npm test&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;cdviz-collector send --run testsuiterun_junit \&lt;/span&gt;
      &lt;span class="s"&gt;--url "${{ secrets.CDVIZ_URL }}" \&lt;/span&gt;
      &lt;span class="s"&gt;--header "Authorization: Bearer ${{ secrets.CDVIZ_TOKEN }}" \&lt;/span&gt;
      &lt;span class="s"&gt;-- npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;CDVIZ_URL&lt;/code&gt; and &lt;code&gt;CDVIZ_TOKEN&lt;/code&gt; as repository secrets (&lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt;). See the &lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/gitlab-ci" rel="noopener noreferrer"&gt;GitLab CI&lt;/a&gt; and &lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/jenkins" rel="noopener noreferrer"&gt;Jenkins&lt;/a&gt; guides for equivalent setups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-Reference with an Artifact (Optional)
&lt;/h2&gt;

&lt;p&gt;If you built a Docker image earlier in the pipeline, pass its digest with &lt;code&gt;--metadata&lt;/code&gt; to link test results to that exact image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdviz-collector send &lt;span class="nt"&gt;--run&lt;/span&gt; testsuiterun_junit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metadata&lt;/span&gt; &lt;span class="s2"&gt;"tested_artifact_id=pkg:oci/my-app@sha256:&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE_SHA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CDVIZ_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$CDVIZ_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CDviz records a &lt;code&gt;testedAgainst&lt;/code&gt; link between the test run and the artifact — useful for tracing which image version a test failure was against.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test trend dashboards&lt;/strong&gt; — pass/fail rates per suite over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step durations&lt;/strong&gt; — how long build, test, and deploy steps take per commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure context&lt;/strong&gt; — which suite failed, on which branch, at which commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact traceability&lt;/strong&gt; — which tests ran against which image (when &lt;code&gt;tested_artifact_id&lt;/code&gt; is set)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>testing</category>
      <category>observability</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Beyond GitHub Stars: Tracking Real Adoption of My Open-Source Projects</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Wed, 25 Mar 2026 15:13:02 +0000</pubDate>
      <link>https://dev.to/davidb31/beyond-github-stars-tracking-real-adoption-of-my-open-source-projects-g29</link>
      <guid>https://dev.to/davidb31/beyond-github-stars-tracking-real-adoption-of-my-open-source-projects-g29</guid>
      <description>&lt;p&gt;I've been maintaining open-source tools for years.&lt;br&gt;
For most of that time, I had no idea if anyone was actually using them.&lt;/p&gt;

&lt;p&gt;Few stars. Not forks. Downloads — the thing that tells you someone ran your binary at least once.&lt;/p&gt;


&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;GitHub shows download counts per release asset — but only the current total. No history.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was there a spike after that blog post you wrote?&lt;/li&gt;
&lt;li&gt;Did your last release get picked up faster than the one before?&lt;/li&gt;
&lt;li&gt;Is your project quietly dying, or just slow to spread?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stars are worse: people star repos they never use. A project with 500 stars might have 3 regular users.&lt;/p&gt;

&lt;p&gt;npm/crates.io/PyPI have their own dashboards — they are nice for libraries and source downloads, but don't show packaged/pre-built release trends and miss the GitHub release binary entirely.&lt;/p&gt;

&lt;p&gt;As an OSS maintainer of binary (not library), you're flying blind on actual adoption.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I wanted
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Historical view: downloads over time, per release&lt;/li&gt;
&lt;li&gt;Trend detection: growing, stagnating, or declining?&lt;/li&gt;
&lt;li&gt;Cross-release comparison: did v0.5.0 get picked up faster than v0.4.0?&lt;/li&gt;
&lt;li&gt;Event correlation: Did that announcement actually move the needle?&lt;/li&gt;
&lt;li&gt;Embeddable: something I can drop into a README&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;download-history — polls the GitHub API every few hours, stores daily snapshots, and visualizes trends across releases. Type owner/repo, get charts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://download-history.cdviz.dev/?repo=davidB%2Fkubectl-view-allocations" rel="noopener noreferrer"&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%2Fkhhcugetbq5owsbfbo8h.gif" alt="Last 60 days interactive chart of kubectl-view-allocations" width="500" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://download-history.cdviz.dev/?repo=jdx%2Fmise" rel="noopener noreferrer"&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%2F93xu83wrc5oiv0mbz2um.gif" alt="Download Distribution by Release chart of mise" width="500" height="111"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Charts are interactive in the browser. Also available as static SVGs for README embeds — updated daily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Downloads&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://api.download-history.cdviz.dev/chart/github.com/owner/repo/60d.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://download-history.cdviz.dev/?repo=davidB%2Fkubectl-view-allocations" rel="noopener noreferrer"&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%2Foftekor9axp1zn2lc4qe.png" alt="Last 60 days static chart of kubectl-view-allocations" width="630" height="644"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Works for both public and private repositories (requires an access token for private repositories).&lt;br&gt;
14-day free trial, no credit card, no account needed. Then 5€/year for up to 2 repositories.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I actually learned from my own data
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/davidB/kubectl-view-allocations" rel="noopener noreferrer"&gt;kubectl-view-allocations&lt;/a&gt; — my most popular project. Unexplained spike in December 2025 / January 2026. No new release. No announcement. Something spread it — a newsletter, someone's blog post, a company's internal tooling. I have no idea what. Without the historical chart, I'd never have noticed it happened at all. ("All time" starts with data capture)&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%2F42lva20delj2i59i2093.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%2F42lva20delj2i59i2093.png" alt="kubectl-view-allocations' charts screenshot" width="589" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ffizer/ffizer" rel="noopener noreferrer"&gt;ffizer&lt;/a&gt; — the opposite story. Nearly zero downloads even after new releases. That's not a product problem. That's an awareness problem. Different diagnosis, different&lt;br&gt;
fix.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://download-history.cdviz.dev/?repo=ffizer%2Fffizer" rel="noopener noreferrer"&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%2Fksvvro7mmmkqgrvgmkrs.png" alt="ffizer's charts screenshot" width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mise.jdx.dev/" rel="noopener noreferrer"&gt;jdx/mise&lt;/a&gt; (not mine, used as reference and because I use it daily) — releases every 1–5 days, used on CI &amp;amp; desktop. Downloads don't spike around releases because users pull it continuously.&lt;br&gt;
Context shapes how you read the charts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://download-history.cdviz.dev/?repo=jdx%2Fmise" rel="noopener noreferrer"&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%2F237lp3r18mbqw2gfm2st.png" alt="mise's charts screenshot" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The data doesn't always give you answers. But it gives you better questions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;If you maintain an OSS project that ships binaries, you probably have the same blind spot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://download-history.cdviz.dev" rel="noopener noreferrer"&gt;download-history.cdviz.dev&lt;/a&gt; — free 14-day trial, no sign-up. I built it for myself; if it's useful to you too, great.&lt;/p&gt;

&lt;p&gt;Next: tracking GitHub Packages (containers, npm, Maven) and possibly GitLab — let me know if that matters to you.&lt;/p&gt;

&lt;p&gt;What metrics do you track for your OSS projects? I'm curious what signals actually matter to other maintainers.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>github</category>
      <category>devops</category>
      <category>rust</category>
    </item>
    <item>
      <title>CDEvents in Action #6: Monitor Every Kubernetes Deployment with One Helm Command</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Tue, 17 Mar 2026 23:00:00 +0000</pubDate>
      <link>https://dev.to/davidb31/cdevents-in-action-6-monitor-every-kubernetes-deployment-with-one-helm-command-3c2o</link>
      <guid>https://dev.to/davidb31/cdevents-in-action-6-monitor-every-kubernetes-deployment-with-one-helm-command-3c2o</guid>
      <description>&lt;p&gt;&lt;em&gt;After this, every Deployment, StatefulSet, and DaemonSet change in your cluster generates a CDEvent automatically — no matter how it was deployed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cdviz.dev/blog/20260311-episode-5-k8s-blind-spot" rel="noopener noreferrer"&gt;Episode #5&lt;/a&gt; explained the gap. This episode closes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CDviz running with &lt;code&gt;cdviz-collector&lt;/code&gt; reachable from inside the cluster&lt;/li&gt;
&lt;li&gt;Kubernetes v1.19+ with kubectl access&lt;/li&gt;
&lt;li&gt;Helm 3&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;cdviz-collector oci://ghcr.io/cdviz-dev/charts/cdviz-collector &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; kubewatch.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; cdviz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-namespace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single flag (&lt;code&gt;kubewatch.enabled=true&lt;/code&gt;) deploys &lt;a href="https://github.com/robusta-dev/kubewatch" rel="noopener noreferrer"&gt;kubewatch&lt;/a&gt; alongside cdviz-collector and wires them together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;kubewatch watches Deployment/StatefulSet/DaemonSet changes cluster-wide (can be configured)&lt;/li&gt;
&lt;li&gt;cdviz-collector receives kubewatch CloudEvents and transforms them to CDEvents&lt;/li&gt;
&lt;li&gt;RBAC is configured automatically (read-only ClusterRole)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/kubewatch" rel="noopener noreferrer"&gt;Kubernetes (via Kubewatch) Integration&lt;/a&gt; for details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify It Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Check both pods are running:&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;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; cdviz
&lt;span class="c"&gt;# NAME                              READY   STATUS&lt;/span&gt;
&lt;span class="c"&gt;# cdviz-collector-xxx               1/1     Running&lt;/span&gt;
&lt;span class="c"&gt;# kubewatch-xxx                     1/1     Running&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Trigger a test deployment:&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;kubectl create deployment test-nginx &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nginx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Confirm the CDEvent arrived:&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;kubectl logs &lt;span class="nt"&gt;-n&lt;/span&gt; cdviz &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cdviz-collector &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20 | &lt;span class="nb"&gt;grep &lt;/span&gt;service.deployed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a &lt;code&gt;service.deployed&lt;/code&gt; event within a few seconds. Check your Grafana dashboard — the deployment appears there too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Clean up:&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;kubectl delete deployment test-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Gets Captured
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kubernetes change&lt;/th&gt;
&lt;th&gt;CDEvent type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deployment created&lt;/td&gt;
&lt;td&gt;&lt;code&gt;service.deployed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image or config updated&lt;/td&gt;
&lt;td&gt;&lt;code&gt;service.upgraded&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment deleted&lt;/td&gt;
&lt;td&gt;&lt;code&gt;service.removed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StatefulSet / DaemonSet changes&lt;/td&gt;
&lt;td&gt;same as above&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The provided transformer for kubewatch event defines the service ID as &lt;code&gt;"{{ namespace }}/{{ resource_name }}/{{ container_name }}"&lt;/code&gt; (where &lt;code&gt;resource_name&lt;/code&gt; is the name of the Deployment, StatefulSet, or DaemonSet) to avoid collisions. In our example: &lt;code&gt;default/test-nginx/nginx&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limiting Scope (Optional)
&lt;/h2&gt;

&lt;p&gt;By default, kubewatch watches all namespaces. To limit to specific ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade cdviz-collector oci://ghcr.io/cdviz-dev/charts/cdviz-collector &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; kubewatch.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; kubewatch.namespaceToWatch&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"production,staging"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; cdviz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduces event volume and avoids noise from system namespaces.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cicd</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>CDEvents in Action #5: The Kubernetes Deployment Blind Spot</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Tue, 10 Mar 2026 23:00:00 +0000</pubDate>
      <link>https://dev.to/davidb31/cdevents-in-action-5-the-kubernetes-deployment-blind-spot-1c3a</link>
      <guid>https://dev.to/davidb31/cdevents-in-action-5-the-kubernetes-deployment-blind-spot-1c3a</guid>
      <description>&lt;p&gt;&lt;em&gt;ArgoCD gives you GitOps visibility. But what about the operator who ran &lt;code&gt;kubectl apply&lt;/code&gt; during an incident? Or the platform team's &lt;code&gt;helm upgrade&lt;/code&gt;? Those are invisible — and they're the ones that tend to cause problems.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gap ArgoCD Leaves
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://cdviz.dev/blog/20251020-episode-4-webhook-transformers" rel="noopener noreferrer"&gt;Episode #4&lt;/a&gt;, you learned to capture CDEvents passively from ArgoCD webhooks — no pipeline changes, instant visibility into GitOps deployments.&lt;/p&gt;

&lt;p&gt;But ArgoCD only sees what ArgoCD manages.&lt;/p&gt;

&lt;p&gt;In a real cluster, deployments happen in multiple ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kubectl apply -f deployment.yaml&lt;/code&gt; — ops team responding to an incident&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;helm upgrade payment-api ./chart&lt;/code&gt; — platform team releasing a patch&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubectl set image deployment/api api=v2.1.0&lt;/code&gt; — someone testing a hotfix&lt;/li&gt;
&lt;li&gt;ArgoCD sync — your GitOps flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the last one shows up in ArgoCD. The first three are invisible.&lt;/p&gt;

&lt;p&gt;This isn't a criticism of ArgoCD. It's the right tool for what it does. The gap is that &lt;strong&gt;your cluster has more activity than your GitOps tool manages&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Native Kubernetes Monitoring Adds
&lt;/h2&gt;

&lt;p&gt;Instead of watching what ArgoCD knows about, you watch the Kubernetes API directly. When any Deployment, StatefulSet, or DaemonSet changes in the cluster — regardless of how it was introduced — a CDEvent is fired.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ArgoCD notifications&lt;/th&gt;
&lt;th&gt;Native k8s monitoring&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trigger&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ArgoCD sync&lt;/td&gt;
&lt;td&gt;Any k8s resource change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ArgoCD-managed apps only&lt;/td&gt;
&lt;td&gt;Every deployment method&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Git context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes — commit, author, PR&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Event volume&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low–medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Webhook + transformer&lt;/td&gt;
&lt;td&gt;One Helm flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rich context per GitOps app&lt;/td&gt;
&lt;td&gt;Complete cluster coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Use Both Together
&lt;/h2&gt;

&lt;p&gt;These approaches complement each other — they don't compete.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ArgoCD notifications&lt;/strong&gt; → &lt;code&gt;service.deployed&lt;/code&gt; events with git commit, author, and PR context for your GitOps apps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native k8s monitoring&lt;/strong&gt; → &lt;code&gt;service.deployed&lt;/code&gt; events for everything else, the kubectl runs, the manual hotfixes, the platform team's Helm releases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combined, you have complete visibility across your cluster. When something breaks, you can see every deployment that happened — not just the ones that went through GitOps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
The one thing to watch: if an app is managed by ArgoCD &lt;em&gt;and&lt;/em&gt; you have native monitoring enabled, you may see duplicate events for ArgoCD syncs. These are easy to filter, but worth knowing up front.&lt;br&gt;
Or you can use a different granularity level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;with ArgoCD: &lt;code&gt;service&lt;/code&gt; is an ArgoCD Application&lt;/li&gt;
&lt;li&gt;with native k8s monitoring: &lt;code&gt;service&lt;/code&gt; is a Container (or a Deployment, StatefulSet, ...)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the strategy used by CDviz's built-in transformers.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>cicd</category>
    </item>
    <item>
      <title>CDEvents in Action #4: Webhook Transformers and Passive Monitoring</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Mon, 20 Oct 2025 13:02:09 +0000</pubDate>
      <link>https://dev.to/davidb31/cdevents-in-action-4-webhook-transformers-and-passive-monitoring-ha0</link>
      <guid>https://dev.to/davidb31/cdevents-in-action-4-webhook-transformers-and-passive-monitoring-ha0</guid>
      <description>&lt;p&gt;&lt;em&gt;Stop modifying every pipeline. Learn how to collect CDEvents from platforms that already send webhooks - GitHub, GitLab, ArgoCD - by transforming their native events automatically.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  From Active to Passive Monitoring
&lt;/h2&gt;

&lt;p&gt;In Episode #3, you learned &lt;strong&gt;active integration&lt;/strong&gt; - modifying pipelines to send CDEvents directly. This works great for new services, but what about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;100+ existing repositories&lt;/strong&gt; you don't want to touch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy pipelines&lt;/strong&gt; that are fragile and risky to modify&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party tools&lt;/strong&gt; (ArgoCD, GitHub Actions) that already send events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance requirements&lt;/strong&gt; demanding complete observability without pipeline changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple platforms&lt;/strong&gt; (GitHub + GitLab + Jenkins) creating integration fatigue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution: &lt;strong&gt;Passive monitoring&lt;/strong&gt; using webhook transformers. Instead of changing pipelines, you configure platforms to send their native webhooks to cdviz-collector, which transforms them into CDEvents automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Active vs. Passive Integration
&lt;/h2&gt;

&lt;p&gt;Understanding the difference between the two approaches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Episode #3 (Active Integration)
├─ Modify each pipeline to send CDEvents
├─ Direct control over event content
├─ Requires touching every pipeline
└─ Best for: New services, custom events

Episode #4 (Passive Integration) ← You are here
├─ Configure webhook once per platform
├─ Automatic transformation of platform events
├─ No pipeline modifications required
└─ Best for: Existing services, standard platform events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: These approaches complement each other. Use passive integration for broad coverage, add active integration for custom events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Webhook Integration Patterns
&lt;/h2&gt;

&lt;p&gt;Learn how to transform native platform events into CDEvents:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Webhook Type&lt;/th&gt;
&lt;th&gt;CDEvents Generated&lt;/th&gt;
&lt;th&gt;Setup Complexity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Repository webhooks&lt;/td&gt;
&lt;td&gt;Pipeline, task, artifact, issue, PR events&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitLab&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Project/group webhooks&lt;/td&gt;
&lt;td&gt;Pipeline, job, artifact, issue, MR events&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ArgoCD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Notifications webhooks&lt;/td&gt;
&lt;td&gt;Deployment, incident, removal events&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Platform Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;GitHub&lt;/th&gt;
&lt;th&gt;GitLab&lt;/th&gt;
&lt;th&gt;ArgoCD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Event Coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Workflows, jobs, releases, issues, PRs&lt;/td&gt;
&lt;td&gt;Pipelines, jobs, releases, issues, MRs&lt;/td&gt;
&lt;td&gt;Sync operations, health status, deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authentication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HMAC-SHA256 signature&lt;/td&gt;
&lt;td&gt;Token header&lt;/td&gt;
&lt;td&gt;Network isolation or Authorization header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transformer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Community (VRL)&lt;/td&gt;
&lt;td&gt;Pro (VRL)&lt;/td&gt;
&lt;td&gt;Community (VRL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Configuration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Repository or Organization webhook&lt;/td&gt;
&lt;td&gt;Project or Group webhook&lt;/td&gt;
&lt;td&gt;Notifications ConfigMap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CDEvents Output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10+ event types&lt;/td&gt;
&lt;td&gt;10+ event types&lt;/td&gt;
&lt;td&gt;4+ event types&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Pattern 1: GitHub Webhook Integration
&lt;/h2&gt;

&lt;p&gt;Collect GitHub repository events and transform them into CDEvents automatically - workflows, jobs, releases, PRs, issues, and branches all generate CDEvents without touching your pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Overview
&lt;/h3&gt;

&lt;p&gt;GitHub webhooks automatically notify cdviz-collector about repository activity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflows &amp;amp; Jobs&lt;/strong&gt; → &lt;code&gt;pipelineRun.*&lt;/code&gt; and &lt;code&gt;taskRun.*&lt;/code&gt; events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Releases &amp;amp; Packages&lt;/strong&gt; → &lt;code&gt;artifact.published&lt;/code&gt; events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull Requests&lt;/strong&gt; → &lt;code&gt;change.*&lt;/code&gt; events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issues&lt;/strong&gt; → &lt;code&gt;ticket.*&lt;/code&gt; events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branches&lt;/strong&gt; → &lt;code&gt;branch.created/deleted&lt;/code&gt; events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;🔗 &lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/github.html" rel="noopener noreferrer"&gt;Complete event mapping and setup guide&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimal Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;cdviz-collector side&lt;/strong&gt; (&lt;code&gt;cdviz-collector.toml&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[remote.transformers-community]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"github"&lt;/span&gt;
&lt;span class="py"&gt;owner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"cdviz-dev"&lt;/span&gt;
&lt;span class="py"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"transformers-community"&lt;/span&gt;

&lt;span class="nn"&gt;[sources.github_webhook]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;transformer_refs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"github_events"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[sources.github_webhook.extractor]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"webhook"&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"000-github"&lt;/span&gt;

&lt;span class="c"&gt;# Verify GitHub signature (HMAC-SHA256)&lt;/span&gt;
&lt;span class="nn"&gt;[sources.github_webhook.extractor.headers.x-hub-signature-256]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"signature"&lt;/span&gt;
&lt;span class="py"&gt;signature_encoding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hex"&lt;/span&gt;
&lt;span class="py"&gt;signature_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"body"&lt;/span&gt;
&lt;span class="py"&gt;signature_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"sha256&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-webhook-secret-here"&lt;/span&gt;

&lt;span class="nn"&gt;[transformers.github_events]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"vrl"&lt;/span&gt;
&lt;span class="py"&gt;template_rfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"transformers-community:///github_events/transformer.vrl"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub side&lt;/strong&gt;: Create webhook at organization or repository level&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payload URL&lt;/strong&gt;: &lt;code&gt;https://your-collector.example.com/webhook/000-github&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret&lt;/strong&gt;: Same token as collector configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events&lt;/strong&gt;: Workflow runs, jobs, releases, PRs, issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;🔗 &lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/github.html#setting-up-github-webhook" rel="noopener noreferrer"&gt;Detailed GitHub webhook setup instructions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Get
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Organization-wide coverage&lt;/strong&gt;: Configure once, all repositories tracked&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Zero pipeline changes&lt;/strong&gt;: Automatic event generation&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Complete lifecycle&lt;/strong&gt;: Queued → started → finished events&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Secure&lt;/strong&gt;: HMAC-SHA256 signature verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;💡 Tip&lt;/strong&gt;: For custom deployment context, combine with &lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/github-action.html" rel="noopener noreferrer"&gt;GitHub Action integration&lt;/a&gt; from Episode #3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: GitLab Webhook Integration
&lt;/h2&gt;

&lt;p&gt;Collect GitLab project events and transform them into CDEvents automatically - pipelines, jobs, releases, MRs, issues, and branches all generate CDEvents without touching your pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Overview
&lt;/h3&gt;

&lt;p&gt;GitLab webhooks automatically notify cdviz-collector about project activity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pipelines &amp;amp; Jobs&lt;/strong&gt; → &lt;code&gt;pipelineRun.*&lt;/code&gt; and &lt;code&gt;taskRun.*&lt;/code&gt; events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Releases &amp;amp; Tags&lt;/strong&gt; → &lt;code&gt;artifact.published&lt;/code&gt; events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge Requests&lt;/strong&gt; → &lt;code&gt;change.*&lt;/code&gt; events (created, merged, abandoned, reviewed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issues&lt;/strong&gt; → &lt;code&gt;ticket.*&lt;/code&gt; events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branches&lt;/strong&gt; → &lt;code&gt;branch.created/deleted&lt;/code&gt; events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;🔗 &lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/gitlab.html" rel="noopener noreferrer"&gt;Complete event mapping and setup guide&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimal Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;cdviz-collector side&lt;/strong&gt; (&lt;code&gt;cdviz-collector.toml&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[remote.transformers-pro]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"github"&lt;/span&gt;
&lt;span class="py"&gt;owner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"cdviz-dev"&lt;/span&gt;
&lt;span class="py"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"transformers-pro"&lt;/span&gt;

&lt;span class="nn"&gt;[sources.gitlab_webhook]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;transformer_refs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"gitlab_events"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[sources.gitlab_webhook.extractor]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"webhook"&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"000-gitlab"&lt;/span&gt;
&lt;span class="py"&gt;headers_to_keep&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"X-Gitlab-Event"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c"&gt;# Verify GitLab token&lt;/span&gt;
&lt;span class="nn"&gt;[[sources.gitlab_webhook.extractor.headers]]&lt;/span&gt;
&lt;span class="py"&gt;header&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"X-Gitlab-Token"&lt;/span&gt;

&lt;span class="nn"&gt;[sources.gitlab_webhook.extractor.headers.rule]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"equals"&lt;/span&gt;
&lt;span class="py"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-secret-token-here"&lt;/span&gt;
&lt;span class="py"&gt;case_sensitive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[transformers.gitlab_events]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"vrl"&lt;/span&gt;
&lt;span class="py"&gt;template_rfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"transformers-pro:///gitlab_events/transformer.vrl"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br&gt;
GitLab transformer is part of the &lt;strong&gt;Pro edition&lt;/strong&gt;. See &lt;a href="https://cdviz.dev" rel="noopener noreferrer"&gt;CDviz Plans&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;GitLab side&lt;/strong&gt;: Create a webhook at the group or project level&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;URL&lt;/strong&gt;: &lt;code&gt;https://your-collector.example.com/webhook/000-gitlab&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret token&lt;/strong&gt;: Same token as collector configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger events&lt;/strong&gt;: Pipeline, job, release, MR, issue events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;🔗 &lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/gitlab.html#setting-up-gitlab-webhook" rel="noopener noreferrer"&gt;Detailed GitLab webhook setup instructions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Get
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Group-wide coverage&lt;/strong&gt;: Configure once, all projects tracked&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Zero pipeline changes&lt;/strong&gt;: Automatic event generation&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Complete lifecycle&lt;/strong&gt;: Queued → started → finished events&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Secure&lt;/strong&gt;: Token-based authentication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;💡 Tip&lt;/strong&gt;: For Kubernetes deployment details, combine with ArgoCD integration (Pattern 3).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: ArgoCD Webhook Integration
&lt;/h2&gt;

&lt;p&gt;Collect ArgoCD application lifecycle events and transform them into CDEvents automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Events ArgoCD Sends
&lt;/h3&gt;

&lt;p&gt;ArgoCD notifications webhook can send:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sync succeeded + healthy&lt;/strong&gt; → &lt;code&gt;service.deployed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync failed/error&lt;/strong&gt; → &lt;code&gt;incident.detected&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health degraded&lt;/strong&gt; → &lt;code&gt;incident.detected&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App deleted&lt;/strong&gt; → &lt;code&gt;service.removed&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Complete mapping&lt;/strong&gt;: See &lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/argocd.html" rel="noopener noreferrer"&gt;ArgoCD Integration documentation&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration: cdviz-collector Side
&lt;/h3&gt;

&lt;p&gt;Create a webhook source that receives and transforms ArgoCD events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# cdviz-collector.toml&lt;/span&gt;

&lt;span class="c"&gt;# Remote transformers repository configuration&lt;/span&gt;
&lt;span class="nn"&gt;[remote.transformers-community]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"github"&lt;/span&gt;
&lt;span class="py"&gt;owner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"cdviz-dev"&lt;/span&gt;
&lt;span class="py"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"transformers-community"&lt;/span&gt;

&lt;span class="nn"&gt;[sources.argocd_webhook]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;transformer_refs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"argocd_notifications"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[sources.argocd_webhook.extractor]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"webhook"&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"000-argocd"&lt;/span&gt;
&lt;span class="py"&gt;headers_to_keep&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="c"&gt;# Optional: Verify Authorization header (when using external endpoints)&lt;/span&gt;
&lt;span class="c"&gt;# [sources.argocd_webhook.extractor.headers.authorization]&lt;/span&gt;
&lt;span class="c"&gt;# type = "secret"&lt;/span&gt;
&lt;span class="c"&gt;# value = "Bearer your-secret-token-here"&lt;/span&gt;

&lt;span class="c"&gt;# Optional: Inject environment metadata from ArgoCD destination&lt;/span&gt;
&lt;span class="nn"&gt;[sources.argocd_webhook.extractor.metadata]&lt;/span&gt;
&lt;span class="py"&gt;environment_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/production/eu-1"&lt;/span&gt;

&lt;span class="c"&gt;# Transformer from transformers-community repository&lt;/span&gt;
&lt;span class="nn"&gt;[transformers.argocd_notifications]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"vrl"&lt;/span&gt;
&lt;span class="py"&gt;template_rfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"transformers-community:///argocd_notifications/transformer.vrl"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Receives ArgoCD webhooks at &lt;code&gt;http://your-collector/webhook/000-argocd&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ Verifies authenticity using Authorization header&lt;/li&gt;
&lt;li&gt;✅ Transforms ArgoCD notifications into CDEvents using VRL transformer&lt;/li&gt;
&lt;li&gt;✅ Generates per-container &lt;code&gt;service.deployed&lt;/code&gt; events automatically&lt;/li&gt;
&lt;li&gt;✅ Routes CDEvents to configured sinks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration: ArgoCD Side
&lt;/h3&gt;

&lt;p&gt;Configure ArgoCD notifications to send webhooks:&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Create Webhook Service
&lt;/h4&gt;

&lt;p&gt;Edit the &lt;code&gt;argocd-notifications-cm&lt;/code&gt; ConfigMap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl edit configmap argocd-notifications-cm &lt;span class="nt"&gt;-n&lt;/span&gt; argocd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add webhook service configuration with authentication:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ConfigMap&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;argocd-notifications-cm&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service.webhook.cdviz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;url: https://your-cdviz-collector.example.com/webhook/000-argocd&lt;/span&gt;
    &lt;span class="s"&gt;headers:&lt;/span&gt;
    &lt;span class="s"&gt;- name: Content-Type&lt;/span&gt;
      &lt;span class="s"&gt;value: application/json&lt;/span&gt;
    &lt;span class="s"&gt;- name: Authorization&lt;/span&gt;
      &lt;span class="s"&gt;value: "Bearer your-secret-token-here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Create Notification Template
&lt;/h4&gt;

&lt;p&gt;Add a unified template that sends the full application state:&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;template.webhook-cdviz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;webhook:&lt;/span&gt;
    &lt;span class="s"&gt;cdviz:&lt;/span&gt;
      &lt;span class="s"&gt;method: POST&lt;/span&gt;
      &lt;span class="s"&gt;body: |&lt;/span&gt;
        &lt;span class="s"&gt;{&lt;/span&gt;
          &lt;span class="s"&gt;"timestamp": "{{ now | date "2006-01-02T15:04:05.000000Z07:00" }}",&lt;/span&gt;
          &lt;span class="s"&gt;"context": {{ toJson .context }},&lt;/span&gt;
          &lt;span class="s"&gt;"app": {{ toJson .app }}&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key point&lt;/strong&gt;: A single template handles all event types. Event detection logic is implemented in the VRL transformer, keeping ArgoCD configuration simple.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Configure Triggers
&lt;/h4&gt;

&lt;p&gt;Add triggers to filter and send relevant events:&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;triggers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;trigger.on-deployed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;- description: Application is synced and healthy. Triggered once per commit.&lt;/span&gt;
      &lt;span class="s"&gt;oncePer: app.status.sync.revision&lt;/span&gt;
      &lt;span class="s"&gt;send: [webhook-cdviz]&lt;/span&gt;
      &lt;span class="s"&gt;when: app.status.operationState != nil and app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy'&lt;/span&gt;

  &lt;span class="na"&gt;trigger.on-health-degraded&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;- description: Application has degraded&lt;/span&gt;
      &lt;span class="s"&gt;send: [webhook-cdviz]&lt;/span&gt;
      &lt;span class="s"&gt;when: app.status.health.status == 'Degraded'&lt;/span&gt;

  &lt;span class="na"&gt;trigger.on-deleted&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;- description: Application is being deleted&lt;/span&gt;
      &lt;span class="s"&gt;send: [webhook-cdviz]&lt;/span&gt;
      &lt;span class="s"&gt;when: app.metadata.deletionTimestamp != nil&lt;/span&gt;

  &lt;span class="na"&gt;trigger.on-sync-failed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;- description: Application syncing has failed&lt;/span&gt;
      &lt;span class="s"&gt;send: [webhook-cdviz]&lt;/span&gt;
      &lt;span class="s"&gt;when: app.status.operationState != nil and app.status.operationState.phase in ['Error', 'Failed']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 4: Enable Notifications for Applications
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Recommended&lt;/strong&gt;: Configure default subscriptions to automatically apply notifications to all applications:&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;subscriptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;- recipients:&lt;/span&gt;
    &lt;span class="s"&gt;- cdviz&lt;/span&gt;
    &lt;span class="s"&gt;triggers:&lt;/span&gt;
    &lt;span class="s"&gt;- on-deployed&lt;/span&gt;
    &lt;span class="s"&gt;- on-health-degraded&lt;/span&gt;
    &lt;span class="s"&gt;- on-deleted&lt;/span&gt;
    &lt;span class="s"&gt;- on-sync-failed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach reduces configuration at the application level and prevents forgetting to enable notifications for new applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing the Integration
&lt;/h3&gt;

&lt;p&gt;Trigger an ArgoCD sync operation:&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;# Sync application manually&lt;/span&gt;
argocd app &lt;span class="nb"&gt;sync &lt;/span&gt;my-app

&lt;span class="c"&gt;# Or push a change that triggers automatic sync&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Update deployment"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected CDEvents&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;service.deployed&lt;/code&gt; - One event per container when sync succeeds and app is healthy&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;incident.detected&lt;/code&gt; - If sync fails or health degrades&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service.removed&lt;/code&gt; - If application is deleted&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What You Get Without Pipeline Changes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Deployment visibility&lt;/strong&gt;: Automatic &lt;code&gt;service.deployed&lt;/code&gt; events for all ArgoCD apps&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Per-container events&lt;/strong&gt;: Separate events for each container in the deployment&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Incident detection&lt;/strong&gt;: Automatic &lt;code&gt;incident.detected&lt;/code&gt; for sync failures and health issues&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;PURL generation&lt;/strong&gt;: Correct Package URL (PURL) for Helm charts, Git repos, OCI images&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;GitOps correlation&lt;/strong&gt;: Links deployments to source commits automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Advantage&lt;/strong&gt;: ArgoCD is the deployment source of truth. Events reflect actual Kubernetes state, not just CI/CD pipeline intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining Active and Passive Integration
&lt;/h2&gt;

&lt;p&gt;The most powerful observability strategy combines both approaches:&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommended Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│ GitHub/GitLab Webhooks (Passive)                        │
│ ├─ All workflows/pipelines → pipelineRun.* events       │
│ ├─ All jobs → taskRun.* events                          │
│ └─ Releases, PRs, issues → artifact.*, change.*, ...    │
└─────────────────────────────────────────────────────────┘
           ↓ Broad coverage, zero pipeline changes
┌─────────────────────────────────────────────────────────┐
│ GitHub Actions/GitLab CI Integration (Active)           │
│ ├─ Custom deployment context → service.deployed         │
│ ├─ Test results → testCaseRun.finished                  │
│ └─ Custom metadata → artifact.published                 │
└─────────────────────────────────────────────────────────┘
           ↓ Detailed context for critical workflows
┌─────────────────────────────────────────────────────────┐
│ ArgoCD Webhooks (Passive)                               │
│ ├─ Actual deployments → service.deployed                │
│ ├─ Health issues → incident.detected                    │
│ └─ Per-container events → fine-grained tracking         │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example: E-Commerce Platform
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scenario&lt;/strong&gt;: 50 microservices, GitHub Actions for CI, ArgoCD for deployment&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Passive Integration&lt;/strong&gt; (broad coverage):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub webhook → All workflows tracked automatically&lt;/li&gt;
&lt;li&gt;ArgoCD webhook → All deployments tracked automatically&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero pipeline modifications&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Active Integration&lt;/strong&gt; (detailed context):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;send-cdevents&lt;/code&gt; action to 5 critical services&lt;/li&gt;
&lt;li&gt;Include custom metadata: feature flags, canary percentage, rollback info&lt;/li&gt;
&lt;li&gt;Send &lt;code&gt;testCaseRun.finished&lt;/code&gt; events with test coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 100% observability coverage via passive integration&lt;/li&gt;
&lt;li&gt;✅ Rich context for critical services via active integration&lt;/li&gt;
&lt;li&gt;✅ Minimal maintenance burden&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Migration Strategy: Passive First, Active Later
&lt;/h2&gt;

&lt;p&gt;Progressively adopt webhook integrations across your organization:&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Enable Passive Monitoring (Week 1)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configure GitHub/GitLab webhook&lt;/strong&gt; at organization/group level&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up cdviz-collector&lt;/strong&gt; with webhook sources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy ArgoCD notifications&lt;/strong&gt; with default subscriptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate events&lt;/strong&gt; appearing in CDviz dashboards&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Effort&lt;/strong&gt;: 1-2 days for platform team&lt;br&gt;
&lt;strong&gt;Impact&lt;/strong&gt;: Immediate visibility across all repositories and deployments&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 2: Validate Coverage (Week 2-3)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Review dashboards&lt;/strong&gt; to identify missing events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check event quality&lt;/strong&gt; (correct subject.id, environment, artifactId)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identify gaps&lt;/strong&gt; where passive integration isn't enough&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document custom event requirements&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Effort&lt;/strong&gt;: 1-2 weeks of observation&lt;br&gt;
&lt;strong&gt;Impact&lt;/strong&gt;: Understand where active integration adds value&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 3: Selective Active Integration (Week 4+)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add active integration&lt;/strong&gt; to top 5 critical services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enhance with custom metadata&lt;/strong&gt; (deployment strategy, SLO targets)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send test events&lt;/strong&gt; (&lt;code&gt;testCaseRun.finished&lt;/code&gt;, &lt;code&gt;testSuiteRun.finished&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure improvement&lt;/strong&gt; in observability quality&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Effort&lt;/strong&gt;: 1-2 days per service&lt;br&gt;
&lt;strong&gt;Impact&lt;/strong&gt;: Rich context for critical services without modifying all pipelines&lt;/p&gt;
&lt;h3&gt;
  
  
  Migration Checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Identify platforms sending webhooks (GitHub, GitLab, ArgoCD)&lt;/li&gt;
&lt;li&gt;[ ] Configure webhook at organization/group level (not per-repository)&lt;/li&gt;
&lt;li&gt;[ ] Set up cdviz-collector with remote transformers&lt;/li&gt;
&lt;li&gt;[ ] Verify webhook signature/token authentication&lt;/li&gt;
&lt;li&gt;[ ] Test webhook delivery with sample events&lt;/li&gt;
&lt;li&gt;[ ] Monitor CDviz dashboards for event coverage&lt;/li&gt;
&lt;li&gt;[ ] Document gaps requiring active integration&lt;/li&gt;
&lt;li&gt;[ ] Selectively add active integration for critical services&lt;/li&gt;
&lt;li&gt;[ ] Measure observability improvement&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Webhook Security Best Practices
&lt;/h2&gt;

&lt;p&gt;Protect your webhook endpoints from unauthorized access:&lt;/p&gt;
&lt;h3&gt;
  
  
  GitHub HMAC-SHA256 Signature Verification
&lt;/h3&gt;

&lt;p&gt;GitHub signs webhooks using HMAC-SHA256. The collector validates signatures automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[sources.github_webhook.extractor.headers.x-hub-signature-256]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"signature"&lt;/span&gt;
&lt;span class="py"&gt;signature_encoding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hex"&lt;/span&gt;
&lt;span class="py"&gt;signature_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"body"&lt;/span&gt;
&lt;span class="py"&gt;signature_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"sha256&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-webhook-secret-here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Verifies webhook came from GitHub&lt;/li&gt;
&lt;li&gt;✅ Prevents replay attacks&lt;/li&gt;
&lt;li&gt;✅ Rejects tampered payloads&lt;/li&gt;
&lt;li&gt;❌ Blocks unauthorized requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;: Use a strong random token (32+ characters) and rotate periodically.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitLab Token Header Validation
&lt;/h3&gt;

&lt;p&gt;GitLab sends a static token in the &lt;code&gt;X-Gitlab-Token&lt;/code&gt; header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[sources.gitlab_webhook.extractor.headers]]&lt;/span&gt;
&lt;span class="py"&gt;header&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"X-Gitlab-Token"&lt;/span&gt;

&lt;span class="nn"&gt;[sources.gitlab_webhook.extractor.headers.rule]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"equals"&lt;/span&gt;
&lt;span class="py"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-secret-token-here"&lt;/span&gt;
&lt;span class="py"&gt;case_sensitive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Verifies webhook came from GitLab&lt;/li&gt;
&lt;li&gt;✅ Simple token-based authentication&lt;/li&gt;
&lt;li&gt;❌ Blocks unauthorized requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;: Use a UUID or strong random token, store in secrets manager.&lt;/p&gt;

&lt;h3&gt;
  
  
  ArgoCD: Network Isolation + Optional Header Auth
&lt;/h3&gt;

&lt;p&gt;ArgoCD offers flexible security options depending on your deployment:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Internal network (simplest)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When collector runs inside the same Kubernetes cluster:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ArgoCD side&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;service.webhook.cdviz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;url: http://cdviz-collector.cdviz:8080/webhook/000-argocd&lt;/span&gt;
  &lt;span class="s"&gt;headers:&lt;/span&gt;
  &lt;span class="s"&gt;- name: Content-Type&lt;/span&gt;
    &lt;span class="s"&gt;value: application/json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Collector side&lt;/strong&gt;: No authentication required (rely on NetworkPolicies)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ No public internet exposure&lt;/li&gt;
&lt;li&gt;✅ Use Kubernetes NetworkPolicies to restrict access&lt;/li&gt;
&lt;li&gt;✅ Simple configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Authorization header (external endpoints or defense-in-depth)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When collector is external or you want additional security:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ArgoCD side&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;service.webhook.cdviz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;url: https://your-cdviz-collector.example.com/webhook/000-argocd&lt;/span&gt;
  &lt;span class="s"&gt;headers:&lt;/span&gt;
  &lt;span class="s"&gt;- name: Content-Type&lt;/span&gt;
    &lt;span class="s"&gt;value: application/json&lt;/span&gt;
  &lt;span class="s"&gt;- name: Authorization&lt;/span&gt;
    &lt;span class="s"&gt;value: "Bearer your-secret-token-here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Collector side&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[sources.argocd_webhook.extractor.headers.authorization]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"secret"&lt;/span&gt;
&lt;span class="py"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Bearer your-secret-token-here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;✅ Works with external collectors&lt;/li&gt;
&lt;li&gt;✅ Standard HTTP Authorization header&lt;/li&gt;
&lt;li&gt;✅ Blocks unauthorized requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;: Use Option 1 (network isolation) when possible, add Option 2 (Authorization header) for external endpoints or defense-in-depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transformer Versioning and Updates
&lt;/h2&gt;

&lt;p&gt;Remote transformers enable centralized updates without changing collector configuration:&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Specific Transformer Versions
&lt;/h3&gt;

&lt;p&gt;Pin to a specific tag for stability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[remote.raw_github]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http"&lt;/span&gt;
&lt;span class="py"&gt;endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://raw.githubusercontent.com"&lt;/span&gt;

&lt;span class="nn"&gt;[transformers.github_events]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"vrl"&lt;/span&gt;
&lt;span class="py"&gt;template_rfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"raw_github:///cdviz-dev/transformers-community/refs/tags/v1.0.0/github_events/transformer.vrl"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using Latest Transformers
&lt;/h3&gt;

&lt;p&gt;Track the latest version automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[remote.transformers-community]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"github"&lt;/span&gt;
&lt;span class="py"&gt;owner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"cdviz-dev"&lt;/span&gt;
&lt;span class="py"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"transformers-community"&lt;/span&gt;

&lt;span class="nn"&gt;[transformers.github_events]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"vrl"&lt;/span&gt;
&lt;span class="py"&gt;template_rfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"transformers-community:///github_events/transformer.vrl"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Trade-off&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Automatic bug fixes and improvements&lt;/li&gt;
&lt;li&gt;❌ Risk of breaking changes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎯 &lt;strong&gt;Passive first&lt;/strong&gt;: Webhook integration provides broad coverage without pipeline changes&lt;/li&gt;
&lt;li&gt;🔧 &lt;strong&gt;Combine approaches&lt;/strong&gt;: Passive for coverage, active for custom context&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Platform coverage&lt;/strong&gt;: GitHub, GitLab, ArgoCD webhooks transform automatically&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;Security built-in&lt;/strong&gt;: HMAC signatures, token validation, network isolation&lt;/li&gt;
&lt;li&gt;📈 &lt;strong&gt;Incremental adoption&lt;/strong&gt;: Start with webhooks, add active integration selectively&lt;/li&gt;
&lt;li&gt;⚙️ &lt;strong&gt;Centralized transformers&lt;/strong&gt;: VRL logic versioned in remote repositories&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Zero pipeline changes&lt;/strong&gt;: Organization-level webhook = all repositories covered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Webhook transformers make CDEvents observability achievable at scale. Configure once, gain visibility across all repositories and deployments without modifying pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/github.html" rel="noopener noreferrer"&gt;GitHub Webhook Integration&lt;/a&gt; - Complete configuration guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/gitlab.html" rel="noopener noreferrer"&gt;GitLab Webhook Integration&lt;/a&gt; - Complete configuration guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/argocd.html" rel="noopener noreferrer"&gt;ArgoCD Notifications Integration&lt;/a&gt; - Complete configuration guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cdviz.dev/docs/cdviz-collector/transformers.html" rel="noopener noreferrer"&gt;Transformers Documentation&lt;/a&gt; - VRL transformer reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./episode-3-cicd-integration.md"&gt;Episode #3: Direct CI/CD Integration&lt;/a&gt; - Active integration patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/github-action.html" rel="noopener noreferrer"&gt;GitHub Actions Integration&lt;/a&gt; - Custom events in workflows&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>argocd</category>
      <category>gitlab</category>
      <category>github</category>
    </item>
    <item>
      <title>CDEvents in Action #3: Direct CI/CD Pipeline Integration</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Tue, 07 Oct 2025 17:41:09 +0000</pubDate>
      <link>https://dev.to/davidb31/cdevents-in-action-3-direct-cicd-pipeline-integration-564a</link>
      <guid>https://dev.to/davidb31/cdevents-in-action-3-direct-cicd-pipeline-integration-564a</guid>
      <description>&lt;p&gt;&lt;em&gt;Automate CDEvents directly from your CI/CD pipelines. Learn three universal integration patterns - from curl+bash to platform plugins - with production-ready best practices for any CI/CD system.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  From Manual to Automated
&lt;/h2&gt;

&lt;p&gt;In Episode #2, you learned how to send CDEvents manually using curl, bash scripts, and &lt;code&gt;cdviz-collector send&lt;/code&gt;. Now it's time to &lt;strong&gt;automate&lt;/strong&gt; this within your CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;Manual event sending works for testing, but production requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic execution&lt;/strong&gt;: Events sent without manual intervention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline integration&lt;/strong&gt;: Events triggered by deployments, tests, builds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context enrichment&lt;/strong&gt;: Include git commits, build numbers, environments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling&lt;/strong&gt;: Graceful failures that don't break pipelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets management&lt;/strong&gt;: Secure authentication without exposing tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This episode focuses on direct integration&lt;/strong&gt; - modifying your pipelines to send CDEvents. In later episodes, we'll explore &lt;strong&gt;passive monitoring&lt;/strong&gt; approaches that collect events without changing every pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Universal Integration Patterns
&lt;/h2&gt;

&lt;p&gt;These patterns work across &lt;strong&gt;all CI/CD systems&lt;/strong&gt; - GitHub Actions, Jenkins, GitLab CI, CircleCI, and any platform with shell support:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pattern A: Curl + bash script&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Direct HTTP POST with bash&lt;/td&gt;
&lt;td&gt;Constrained environments, minimal dependencies&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pattern B: cdviz-collector send&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Install collector CLI, send events via command&lt;/td&gt;
&lt;td&gt;Multiple destinations, flexibility&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pattern C: Platform-specific plugin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use native actions/plugins/orbs&lt;/td&gt;
&lt;td&gt;Simplest when available&lt;/td&gt;
&lt;td&gt;Very Low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This ordering reflects &lt;strong&gt;progressive complexity&lt;/strong&gt;: Pattern A shows the foundational HTTP mechanics, Pattern B adds tooling benefits, and Pattern C provides platform integration convenience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern Decision Matrix
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use Pattern A (curl + bash) if&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your environment restricts installing binaries&lt;/li&gt;
&lt;li&gt;You only need simple HTTP POST&lt;/li&gt;
&lt;li&gt;You want minimal dependencies&lt;/li&gt;
&lt;li&gt;You're comfortable maintaining bash scripts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Pattern B (cdviz-collector send) if&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to send to various destination types (HTTP, Kafka, Database, S3)&lt;/li&gt;
&lt;li&gt;You want automatic ID and timestamp generation&lt;/li&gt;
&lt;li&gt;You need advanced features (transformers, signatures, validation)&lt;/li&gt;
&lt;li&gt;You're building for long-term flexibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Pattern C (platform-specific plugin) if&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your platform has a native CDEvents plugin/action&lt;/li&gt;
&lt;li&gt;You want the simplest possible setup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Platform Compatibility Matrix
&lt;/h2&gt;

&lt;p&gt;Before diving into detailed examples, here's how each pattern maps to popular CI/CD platforms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Pattern A (Curl + Bash)&lt;/th&gt;
&lt;th&gt;Pattern B (cdviz-collector)&lt;/th&gt;
&lt;th&gt;Pattern C (Plugin)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jenkins&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❓ Plugins TBD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitLab CI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ send-cdevents@v1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CircleCI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❓ Orbs TBD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Azure DevOps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❓ Tasks TBD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bitbucket Pipelines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generic/Custom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ If bash available&lt;/td&gt;
&lt;td&gt;✅ If bash/curl available&lt;/td&gt;
&lt;td&gt;❌ No native&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: Patterns A and B work universally on any platform with shell access. Pattern C provides the simplest experience but depends on platform-specific plugin availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern Examples: From Curl to Plugin
&lt;/h2&gt;

&lt;p&gt;The following sections show one detailed example per pattern, demonstrating the progression from foundational HTTP mechanics to platform-integrated convenience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern A: Curl + Bash (Jenkins) - Most Portable
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: Restricted environments, can't install binaries, minimal dependencies&lt;/p&gt;

&lt;p&gt;Jenkins Declarative Pipeline using direct HTTP POST with bash - works anywhere with curl and openssl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Jenkinsfile&lt;/span&gt;
&lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;

    &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CDEVENTS_ENDPOINT_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cdevents-endpoint-url'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;API_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api-token'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;stages&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Deploy'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'./deploy.sh'&lt;/span&gt;

                &lt;span class="c1"&gt;// Send deployment event with curl&lt;/span&gt;
                &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'''
                    cat &amp;gt; body.json &amp;lt;&amp;lt;EOF
                    {
                      "context": {
                        "version": "0.4.1",
                        "source": "${BUILD_URL}",
                        "type": "dev.cdevents.service.deployed.0.2.0",
                        "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
                      },
                      "subject": {
                        "id": "my-namespace/my-service",
                        "type": "service",
                        "content": {
                          "environment": {"id": "production"},
                          "artifactId": "pkg:oci/my-service@sha256:{digest}?repository_url=docker.io/my-org/my-service&amp;amp;tag={version}"
                        }
                      }
                    }
                    EOF

                    SIGNATURE=$(openssl dgst -hex -sha256 -hmac "$API_TOKEN" body.json)
                    SIGNATURE=${SIGNATURE#* }

                    curl -X POST "$CDEVENTS_ENDPOINT_URL" \
                      -H "Content-Type: application/json" \
                      -H "X-Signature: sha256=$SIGNATURE" \
                      --data @body.json

                    rm body.json
                '''&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&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;&lt;strong&gt;What this does&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Zero external dependencies (uses curl + openssl)&lt;/li&gt;
&lt;li&gt;✅ Works in any environment with bash&lt;/li&gt;
&lt;li&gt;✅ Full control over event payload and HTTP request&lt;/li&gt;
&lt;li&gt;✅ HMAC signature authentication for secure delivery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Adaptation for other platforms&lt;/strong&gt;: Replace Jenkins-specific variables (&lt;code&gt;$BUILD_URL&lt;/code&gt;, &lt;code&gt;$GIT_COMMIT&lt;/code&gt;) with your platform's equivalents. The curl command works identically across all systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern B: cdviz-collector send (GitLab CI) - More Flexible
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: Need multiple destinations, advanced configuration, automatic ID/timestamp generation&lt;/p&gt;

&lt;p&gt;GitLab CI using &lt;code&gt;cdviz-collector&lt;/code&gt; CLI for enhanced features:&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;# .gitlab-ci.yml&lt;/span&gt;
&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;CDEVENTS_ENDPOINT_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CDEVENTS_ENDPOINT_URL&lt;/span&gt; &lt;span class="c1"&gt;# From CI/CD variables&lt;/span&gt;

&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm install&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;

&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Install cdviz-collector (cache this in practice)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -LO https://github.com/cdviz-dev/cdviz-collector/releases/latest/download/cdviz-collector-linux-x86_64&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;chmod +x cdviz-collector-linux-x86_64&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mv cdviz-collector-linux-x86_64 /usr/local/bin/cdviz-collector&lt;/span&gt;

    &lt;span class="c1"&gt;# Deploy application&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./deploy.sh&lt;/span&gt;

    &lt;span class="c1"&gt;# Send deployment event (minimal format - collector generates ID/timestamp)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;cdviz-collector send --data '{&lt;/span&gt;
        &lt;span class="s"&gt;"context": {&lt;/span&gt;
          &lt;span class="s"&gt;"version": "0.4.1",&lt;/span&gt;
          &lt;span class="s"&gt;"source": "'"${CI_PIPELINE_URL}"'",&lt;/span&gt;
          &lt;span class="s"&gt;"type": "dev.cdevents.service.deployed.0.2.0"&lt;/span&gt;
        &lt;span class="s"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"subject": {&lt;/span&gt;
          &lt;span class="s"&gt;"id": "/my-namespace/'"${CI_PROJECT_NAME}"'",&lt;/span&gt;
          &lt;span class="s"&gt;"type": "service",&lt;/span&gt;
          &lt;span class="s"&gt;"content": {&lt;/span&gt;
            &lt;span class="s"&gt;"environment": {"id": "production"},&lt;/span&gt;
            &lt;span class="s"&gt;"artifactId": "pkg:oci/'"${CI_PROJECT_NAME}"'@sha256:{digest}?repository_url=registry.example.com/'"${CI_PROJECT_NAME}"'&amp;amp;tag='"${CI_COMMIT_SHORT_SHA}"'"&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;
      &lt;span class="s"&gt;}' --url "${CDEVENTS_ENDPOINT_URL}" \&lt;/span&gt;
         &lt;span class="s"&gt;--header "Authorization: Bearer ${CDEVENTS_AUTH_TOKEN}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Automatic ID and timestamp generation (no manual &lt;code&gt;context.id&lt;/code&gt; or &lt;code&gt;context.timestamp&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;✅ Built-in validation of CDEvent format&lt;/li&gt;
&lt;li&gt;✅ Support for various destination types (HTTP, Kafka, S3, Database, and more)&lt;/li&gt;
&lt;li&gt;✅ Advanced authentication (HMAC, Bearer tokens, custom headers)&lt;/li&gt;
&lt;li&gt;✅ Transformers and data manipulation capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Adaptation for other platforms&lt;/strong&gt;: Install &lt;code&gt;cdviz-collector&lt;/code&gt; binary, replace GitLab variables (&lt;code&gt;$CI_PIPELINE_URL&lt;/code&gt;, &lt;code&gt;$CI_COMMIT_SHA&lt;/code&gt;) with your platform's equivalents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://cdviz.dev/docs/cdviz-collector/send.html" rel="noopener noreferrer"&gt;→ cdviz-collector send documentation&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern C: Platform Plugin (GitHub Actions) - Easiest
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: Simplest approach for GitHub users, platform-native integration&lt;/p&gt;

&lt;p&gt;GitHub Actions using the &lt;code&gt;send-cdevents&lt;/code&gt; action for zero-configuration setup:&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/deploy.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;Deploy Service&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy application&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Deploying to production..."&lt;/span&gt;
          &lt;span class="s"&gt;# Your deployment commands here&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;Send deployment event&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;cdviz-dev/send-cdevents@v1&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;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;{&lt;/span&gt;
              &lt;span class="s"&gt;"context": {&lt;/span&gt;
                &lt;span class="s"&gt;"version": "0.4.1",&lt;/span&gt;
                &lt;span class="s"&gt;"source": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",&lt;/span&gt;
                &lt;span class="s"&gt;"type": "dev.cdevents.service.deployed.0.2.0"&lt;/span&gt;
              &lt;span class="s"&gt;},&lt;/span&gt;
              &lt;span class="s"&gt;"subject": {&lt;/span&gt;
                &lt;span class="s"&gt;"id": "/my-namespace/${{ github.event.repository.name }}",&lt;/span&gt;
                &lt;span class="s"&gt;"type": "service",&lt;/span&gt;
                &lt;span class="s"&gt;"content": {&lt;/span&gt;
                  &lt;span class="s"&gt;"environment": {"id": "production"},&lt;/span&gt;
                  &lt;span class="s"&gt;"artifactId": "pkg:oci/${{ github.event.repository.name }}@sha256:{digest}?repository_url=ghcr.io/${{ github.repository }}&amp;amp;tag=${{ github.ref_name }}"&lt;/span&gt;
                &lt;span class="s"&gt;}&lt;/span&gt;
              &lt;span class="s"&gt;}&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CDEVENTS_ENDPOINT_URL }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Automatic ID and timestamp generation&lt;/li&gt;
&lt;li&gt;✅ Built-in validation of CDEvent format&lt;/li&gt;
&lt;li&gt;✅ Native GitHub Actions integration&lt;/li&gt;
&lt;li&gt;✅ Secure secrets management&lt;/li&gt;
&lt;li&gt;✅ Zero installation required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Adaptation for other platforms&lt;/strong&gt;: Check if your platform has a native CDEvents plugin (see Platform Compatibility Matrix). If not, use Pattern A or B.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/github-action.html" rel="noopener noreferrer"&gt;→ send-cdevents action documentation&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete Multi-Event Workflow Example
&lt;/h2&gt;

&lt;p&gt;Real-world CI/CD pipelines generate multiple events tracking the entire software delivery lifecycle. This GitHub Actions example shows a simplified instrumentation:&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/complete-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;Complete 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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-test-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;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@v5&lt;/span&gt;

      &lt;span class="c1"&gt;# Build started&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build started event&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;cdviz-dev/send-cdevents@v1&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;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;{&lt;/span&gt;
              &lt;span class="s"&gt;"context": {&lt;/span&gt;
                &lt;span class="s"&gt;"version": "0.4.1",&lt;/span&gt;
                &lt;span class="s"&gt;"source": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",&lt;/span&gt;
                &lt;span class="s"&gt;"type": "dev.cdevents.taskrun.started.0.2.0"&lt;/span&gt;
              &lt;span class="s"&gt;},&lt;/span&gt;
              &lt;span class="s"&gt;"subject": {&lt;/span&gt;
                &lt;span class="s"&gt;"id": "build-job/${{ github.run_id }}",&lt;/span&gt;
                &lt;span class="s"&gt;"type": "taskRun",&lt;/span&gt;
                &lt;span class="s"&gt;"content": {&lt;/span&gt;
                  &lt;span class="s"&gt;"taskName": "build"&lt;/span&gt;
                &lt;span class="s"&gt;}&lt;/span&gt;
              &lt;span class="s"&gt;}&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CDEVENTS_ENDPOINT_URL }}&lt;/span&gt;

      &lt;span class="c1"&gt;# Build application&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm install&lt;/span&gt;
          &lt;span class="s"&gt;npm run build&lt;/span&gt;

      &lt;span class="c1"&gt;# Build finished&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build finished event&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cdviz-dev/send-cdevents@v1&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;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;{&lt;/span&gt;
              &lt;span class="s"&gt;"context": {&lt;/span&gt;
                &lt;span class="s"&gt;"version": "0.4.1",&lt;/span&gt;
                &lt;span class="s"&gt;"source": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",&lt;/span&gt;
                &lt;span class="s"&gt;"type": "dev.cdevents.taskrun.finished.0.2.0"&lt;/span&gt;
              &lt;span class="s"&gt;},&lt;/span&gt;
              &lt;span class="s"&gt;"subject": {&lt;/span&gt;
                &lt;span class="s"&gt;"id": "build-job/${{ github.run_id }}",&lt;/span&gt;
                &lt;span class="s"&gt;"type": "taskRun",&lt;/span&gt;
                &lt;span class="s"&gt;"content": {&lt;/span&gt;
                  &lt;span class="s"&gt;"taskName": "build",&lt;/span&gt;
                  &lt;span class="s"&gt;"outcome": "${{ job.status }}"&lt;/span&gt;
                &lt;span class="s"&gt;}&lt;/span&gt;
              &lt;span class="s"&gt;}&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CDEVENTS_ENDPOINT_URL }}&lt;/span&gt;

      &lt;span class="c1"&gt;# Test execution&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="c1"&gt;# Test finished&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;Test finished event&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cdviz-dev/send-cdevents@v1&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;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;{&lt;/span&gt;
              &lt;span class="s"&gt;"context": {&lt;/span&gt;
                &lt;span class="s"&gt;"version": "0.4.1",&lt;/span&gt;
                &lt;span class="s"&gt;"source": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",&lt;/span&gt;
                &lt;span class="s"&gt;"type": "dev.cdevents.testcaserun.finished.0.2.0"&lt;/span&gt;
              &lt;span class="s"&gt;},&lt;/span&gt;
              &lt;span class="s"&gt;"subject": {&lt;/span&gt;
                &lt;span class="s"&gt;"id": "test-suite/${{ github.run_id }}",&lt;/span&gt;
                &lt;span class="s"&gt;"type": "testCaseRun",&lt;/span&gt;
                &lt;span class="s"&gt;"content": {&lt;/span&gt;
                  &lt;span class="s"&gt;"outcome": "${{ steps.test.outcome }}"&lt;/span&gt;
                &lt;span class="s"&gt;}&lt;/span&gt;
              &lt;span class="s"&gt;}&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CDEVENTS_ENDPOINT_URL }}&lt;/span&gt;

      &lt;span class="c1"&gt;# Deploy&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;Deploy to production&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.test.outcome == 'success'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Deploying to production..."&lt;/span&gt;

      &lt;span class="c1"&gt;# Deployment event&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;Deployment event&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.test.outcome == 'success'&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;cdviz-dev/send-cdevents@v1&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;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;{&lt;/span&gt;
              &lt;span class="s"&gt;"context": {&lt;/span&gt;
                &lt;span class="s"&gt;"version": "0.4.1",&lt;/span&gt;
                &lt;span class="s"&gt;"source": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",&lt;/span&gt;
                &lt;span class="s"&gt;"type": "dev.cdevents.service.deployed.0.2.0"&lt;/span&gt;
              &lt;span class="s"&gt;},&lt;/span&gt;
              &lt;span class="s"&gt;"subject": {&lt;/span&gt;
                &lt;span class="s"&gt;"id": "${{ github.event.repository.name }}/production",&lt;/span&gt;
                &lt;span class="s"&gt;"type": "service",&lt;/span&gt;
                &lt;span class="s"&gt;"content": {&lt;/span&gt;
                  &lt;span class="s"&gt;"environment": {"id": "production"},&lt;/span&gt;
                  &lt;span class="s"&gt;"artifactId": "pkg:oci/${{ github.event.repository.name }}@sha256:{digest}?repository_url=ghcr.io/${{ github.repository }}&amp;amp;tag=${{ github.ref_name }}"&lt;/span&gt;
                &lt;span class="s"&gt;}&lt;/span&gt;
              &lt;span class="s"&gt;}&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CDEVENTS_ENDPOINT_URL }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;⚠️ Note on Complete Instrumentation&lt;/strong&gt;: This example shows &lt;strong&gt;simplified&lt;/strong&gt; instrumentation with 4 events. A &lt;strong&gt;fully instrumented&lt;/strong&gt; CI/CD pipeline would generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pipelinerun.started&lt;/code&gt; + &lt;code&gt;pipelinerun.finished&lt;/code&gt; (workflow level)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;taskrun.started&lt;/code&gt; + &lt;code&gt;taskrun.finished&lt;/code&gt; (for each job/stage)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;testsuiterun.started&lt;/code&gt; + &lt;code&gt;testsuiterun.finished&lt;/code&gt; (test suite execution)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;testrun.started&lt;/code&gt; + &lt;code&gt;testrun.finished&lt;/code&gt; (individual test cases)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;artifact.packaged&lt;/code&gt; + &lt;code&gt;artifact.published&lt;/code&gt; (when building/pushing OCI images)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full instrumentation provides complete observability but requires more integration work. Start with key lifecycle events (build, test, deploy) and expand based on observability needs.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Interested in a dedicated article on complete pipeline instrumentation patterns? Let us know (via comment) !&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication &amp;amp; Security
&lt;/h2&gt;

&lt;p&gt;All patterns support secure delivery with platform-native secrets management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Authentication to Your Pattern
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pattern A (Curl + Bash)&lt;/strong&gt; - Already includes HMAC signature (see Jenkins example above).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern B (cdviz-collector send)&lt;/strong&gt; - Add Bearer token header:&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;# GitLab CI example - add authentication&lt;/span&gt;
&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ... (existing deployment steps)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;cdviz-collector send --data '{...}' \&lt;/span&gt;
        &lt;span class="s"&gt;--url "${CDEVENTS_ENDPOINT_URL}" \&lt;/span&gt;
        &lt;span class="s"&gt;--header "Authorization: Bearer ${CDEVENTS_AUTH_TOKEN}"  # ← Add this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pattern C (GitHub Actions)&lt;/strong&gt; - Add HMAC signature via config:&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 Actions - add HMAC signature&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;Send with signature&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;cdviz-dev/send-cdevents@v1&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;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;{...}  # Your event payload&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CDEVENTS_ENDPOINT_URL }}&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt; &lt;span class="c1"&gt;# ← Add this section&lt;/span&gt;
      &lt;span class="s"&gt;[sinks.http.headers.x-signature-256]&lt;/span&gt;
      &lt;span class="s"&gt;type = "signature"&lt;/span&gt;
      &lt;span class="s"&gt;algorithm = "sha256"&lt;/span&gt;
      &lt;span class="s"&gt;prefix = "sha256="&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;CDVIZ_COLLECTOR__SINKS__HTTP__HEADERS__X_SIGNATURE_256__TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CDEVENTS_ENDPOINT_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Error Handling
&lt;/h3&gt;

&lt;p&gt;Prevent CDEvents delivery failures from breaking your pipeline:&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 Actions&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;Send event&lt;/span&gt;
  &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# ← Pipeline continues even if event fails&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;cdviz-dev/send-cdevents@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Jenkins&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'cdviz-collector send ...'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Failed to send CDEvent: ${e}"&lt;/span&gt;  &lt;span class="c1"&gt;// Log but don't fail&lt;/span&gt;
&lt;span class="o"&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 yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitLab CI&lt;/span&gt;
&lt;span class="na"&gt;send_event&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cdviz-collector send ... || &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# ← Continue on failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Opinionated CDEvents Best Practices
&lt;/h2&gt;

&lt;p&gt;When creating CDEvents, choosing good values for key fields improves observability and event correlation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Some examples in this article simplify these rules for clarity. Follow these practices in production deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  context.source - Event Origin
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Rule&lt;/strong&gt;: Use the &lt;strong&gt;URL of the specific workflow/job execution&lt;/strong&gt;, not just the repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: Enables tracing events back to the exact pipeline run that generated them.&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;# ✅ Good - Specific workflow run&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/myorg/myrepo/actions/runs/12345"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://gitlab.com/myorg/myrepo/-/pipelines/67890"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://jenkins.example.com/job/deploy/123"&lt;/span&gt;

&lt;span class="c1"&gt;# ❌ Avoid - Too generic - conflict when aggregated within a bigger scope&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github.com/myorg/myrepo"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;myrepo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  subject.id - Event Subject Identifier
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Rule&lt;/strong&gt;: Use &lt;strong&gt;unique, hierarchical identifiers&lt;/strong&gt; scoped to your organization where all events are collected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: Enables filtering and grouping events by service, environment, or component when events from multiple sources are aggregated together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Do NOT use &lt;code&gt;subject.source&lt;/code&gt; - it's confusing and optional. Instead, make &lt;code&gt;subject.id&lt;/code&gt; unique within your organization's scope and let &lt;code&gt;context.source&lt;/code&gt; identify the event origin.&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;# ✅ Good - Unique within organization, hierarchical, semantic&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject.id"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-service/production"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject.id"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frontend/staging/web-app"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject.id"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend/dev/api-gateway"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject.id"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/team/my-service/production"&lt;/span&gt;  &lt;span class="c1"&gt;# Include team for larger orgs&lt;/span&gt;

&lt;span class="c1"&gt;# ❌ Avoid - Not unique or too generic within your scope&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject.id"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;550e8400-e29b-41d4-a716-446655440000"&lt;/span&gt;  &lt;span class="c1"&gt;# UUID (use context.id for that)&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject.id"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run-12345"&lt;/span&gt;  &lt;span class="c1"&gt;# Run-specific (use context.id for that)&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject.id"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production"&lt;/span&gt;  &lt;span class="c1"&gt;# Too generic - which service?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  environment.id - Deployment Environment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Rule&lt;/strong&gt;: Use &lt;strong&gt;consistent, lowercase environment names&lt;/strong&gt; across all services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: Enables environment-level dashboards and alerts.&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;# ✅ Good - Consistent naming&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;environment"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;environment"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staging"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;environment"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ❌ Avoid - Inconsistent naming&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;environment"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;     &lt;span class="c1"&gt;# vs "production"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;environment"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STAGING"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# vs "staging"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;environment"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev-123"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# too specific&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  artifactId - Package URL (PURL)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Rule&lt;/strong&gt;: Follow the &lt;a href="https://github.com/package-url/purl-spec" rel="noopener noreferrer"&gt;Package URL specification&lt;/a&gt; for your artifact type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: Enables universal artifact identification and dependency tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common Patterns&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="c1"&gt;# OCI images (Docker/container registries)&lt;/span&gt;
&lt;span class="c1"&gt;# Note: OCI type doesn't support namespace - use query params for registry/repo&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artifactId"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pkg:oci/my-app@sha256:abc123def456...?repository_url=ghcr.io/myorg/my-app&amp;amp;tag=v1.2.3"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artifactId"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pkg:oci/nginx@sha256:def456abc123...?repository_url=docker.io/library/nginx&amp;amp;tag=latest"&lt;/span&gt;

&lt;span class="c1"&gt;# NPM packages&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artifactId"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pkg:npm/lodash@4.17.21"&lt;/span&gt;

&lt;span class="c1"&gt;# Maven artifacts&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artifactId"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pkg:maven/org.springframework/spring-core@5.3.10"&lt;/span&gt;

&lt;span class="c1"&gt;# Generic packages&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artifactId"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pkg:generic/my-app@1.2.3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common Gotchas&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Digest vs Tag&lt;/strong&gt;: Use digest (&lt;code&gt;@sha256:...&lt;/code&gt;) for immutability, not commit SHA - this is the image digest, not the source code commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCI Namespace Limitation&lt;/strong&gt;: &lt;code&gt;pkg:oci/&lt;/code&gt; type does NOT support namespace in the path - use &lt;code&gt;repository_url&lt;/code&gt; query parameter instead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registry Encoding&lt;/strong&gt;: OCI requires &lt;code&gt;repository_url&lt;/code&gt; query param; other types may encode registry differently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Semantics&lt;/strong&gt;: For OCI, the version is the &lt;strong&gt;image digest&lt;/strong&gt;, not the git commit that built it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type-Specific Rules&lt;/strong&gt;: Each PURL type (&lt;code&gt;pkg:oci/&lt;/code&gt;, &lt;code&gt;pkg:npm/&lt;/code&gt;, etc.) has unique encoding rules - always check the spec&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/package-url/purl-spec" rel="noopener noreferrer"&gt;→ Full PURL specification&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic ID and Timestamp Generation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Rule&lt;/strong&gt;: Let tools generate &lt;code&gt;context.id&lt;/code&gt; and &lt;code&gt;context.timestamp&lt;/code&gt; when possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: Avoids manual errors and ensures content-based deduplication.&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;"context"&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;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.4.1"&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="err"&gt;✅&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Omit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id"&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="err"&gt;cdviz-collector&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;generates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;content-based&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/myorg/myrepo/actions/runs/12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev.cdevents.service.deployed.0.2.0"&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="err"&gt;✅&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Omit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timestamp"&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="err"&gt;cdviz-collector&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;uses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;current&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;time&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;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;🎯 &lt;strong&gt;Three universal patterns&lt;/strong&gt;: Curl+bash → cdviz-collector → platform plugins (progressive complexity)&lt;br&gt;
🔧 &lt;strong&gt;One pattern per platform&lt;/strong&gt;: Jenkins (Pattern A), GitLab CI (Pattern B), GitHub Actions (Pattern C)&lt;br&gt;
📊 &lt;strong&gt;Cross-platform compatibility&lt;/strong&gt;: All patterns work on any platform with shell access&lt;br&gt;
🔒 &lt;strong&gt;Security built-in&lt;/strong&gt;: Native secrets management, HMAC signatures, error handling&lt;br&gt;
📝 &lt;strong&gt;CDEvents best practices&lt;/strong&gt;: Use workflow URLs for source, hierarchical subject IDs, PURL for artifacts&lt;br&gt;
⚙️ &lt;strong&gt;Auto-generated fields&lt;/strong&gt;: Let tools generate context.id and context.timestamp&lt;br&gt;
📈 &lt;strong&gt;Start simple&lt;/strong&gt;: Begin with key lifecycle events (build, test, deploy), expand as needed&lt;/p&gt;

&lt;p&gt;Direct CI/CD integration gives you full control over CDEvents generation. These patterns work universally, letting you implement observability regardless of your tooling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Try these experiments&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with Pattern A (curl+bash) to understand the HTTP mechanics&lt;/li&gt;
&lt;li&gt;Upgrade to Pattern B (cdviz-collector) for production flexibility&lt;/li&gt;
&lt;li&gt;Use Pattern C (platform plugin) if available on your platform&lt;/li&gt;
&lt;li&gt;Apply CDEvents best practices (workflow URLs, hierarchical IDs, PURL)&lt;/li&gt;
&lt;li&gt;Add authentication (HMAC signatures) for production deployments&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Coming next in Episode #4&lt;/strong&gt;: Advanced patterns including Jenkins plugins, shared libraries, multi-pipeline orchestration, and reusable components for enterprise CI/CD.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also explore&lt;/strong&gt;: Episodes 5-7 will cover &lt;strong&gt;passive monitoring&lt;/strong&gt; - collecting events without modifying pipelines, perfect for legacy systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cdviz.dev/docs/cdviz-collector/integrations/github-action.html" rel="noopener noreferrer"&gt;send-cdevents GitHub Action&lt;/a&gt; - Complete action reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cdviz.dev/docs/cdviz-collector/send.html" rel="noopener noreferrer"&gt;cdviz-collector send documentation&lt;/a&gt; - CLI command reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./episode-2-send-first-cdevent.md"&gt;Episode #2: Send Your First CDEvent&lt;/a&gt; - Foundation for these patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cdevents.dev" rel="noopener noreferrer"&gt;CDEvents Specification&lt;/a&gt; - Complete event standard reference&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cicd</category>
      <category>githubactions</category>
      <category>gitlab</category>
    </item>
    <item>
      <title>CDEvents in Action #2: Send Your First CDEvent</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Wed, 01 Oct 2025 14:10:01 +0000</pubDate>
      <link>https://dev.to/davidb31/cdevents-in-action-2-send-your-first-cdevent-57hm</link>
      <guid>https://dev.to/davidb31/cdevents-in-action-2-send-your-first-cdevent-57hm</guid>
      <description>&lt;p&gt;&lt;em&gt;You've tested receiving CDEvents. Now learn how to send them from your pipelines with three progressive approaches: basic curl for testing, production bash scripts with security, and the recommended cdviz-collector send command.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Producer Problem
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="//./episode-1-simulate-consumer.md"&gt;Episode #1&lt;/a&gt;, you learned how to simulate receiving CDEvents. Now you need to &lt;strong&gt;send&lt;/strong&gt; events from your actual pipelines and tools.&lt;/p&gt;

&lt;p&gt;But sending events properly means handling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correct CDEvent format&lt;/strong&gt;: Valid JSON structure and required fields&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: Authentication and request signing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique identifiers&lt;/strong&gt;: Content-based IDs for deduplication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accurate timestamps&lt;/strong&gt;: ISO 8601 format with timezone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple destinations&lt;/strong&gt;: Database, webhooks, Kafka, S3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Starting with hardcoded curl commands helps you understand the basics. Then you'll graduate to production-ready solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Setup: Test Your Sending
&lt;/h2&gt;

&lt;p&gt;Before we start sending events, let's set up a local test consumer to validate what we send. As we learned in episode 1, &lt;code&gt;cdviz-collector connect&lt;/code&gt; makes this easy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Launch a Local Test Consumer
&lt;/h3&gt;

&lt;p&gt;Create a minimal configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# cdviz-collector-debug.toml&lt;/span&gt;
&lt;span class="nn"&gt;[http]&lt;/span&gt;
&lt;span class="py"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.0.0.0"&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;

&lt;span class="nn"&gt;[sinks.debug]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"debug"&lt;/span&gt;
&lt;span class="py"&gt;format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"json"&lt;/span&gt;
&lt;span class="py"&gt;destination&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"stdout"&lt;/span&gt;

&lt;span class="nn"&gt;[sources.cdevents_webhook]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[sources.cdevents_webhook.extractor]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"webhook"&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"000-cdevents"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch the collector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdviz-collector connect &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--config&lt;/span&gt; ./cdviz-collector-debug.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Accepts CDEvents at &lt;code&gt;http://localhost:8080/webhook/000-cdevents&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ Validates CDEvent format and rejects invalid events&lt;/li&gt;
&lt;li&gt;✅ Shows events in real-time on stdout as they arrive&lt;/li&gt;
&lt;li&gt;✅ Provides immediate feedback for your testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep this running in one terminal while you test sending events from another terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Approaches to Send CDEvents
&lt;/h2&gt;

&lt;p&gt;Each approach serves different needs and skill levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Setup Time&lt;/th&gt;
&lt;th&gt;Security&lt;/th&gt;
&lt;th&gt;Flexibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Basic curl&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Learning, quick testing&lt;/td&gt;
&lt;td&gt;30 seconds&lt;/td&gt;
&lt;td&gt;❌ None&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Production bash script&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CI/CD without dependencies&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;td&gt;✅ HMAC&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;cdviz-collector send&lt;/strong&gt; ⭐&lt;/td&gt;
&lt;td&gt;Production use, multiple destinations&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;td&gt;✅ Full auth&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's explore each approach and understand when to use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: Basic Curl - Understanding the Structure
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: Learning CDEvents structure, quick prototyping, understanding HTTP basics&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perfect for&lt;/strong&gt;: Developers new to CDEvents who want hands-on experience&lt;/p&gt;

&lt;h3&gt;
  
  
  Send a Basic CDEvent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Send a deployment event with hardcoded values&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/webhook/000-cdevents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "context": {
      "version": "0.4.1",
      "id": "test-123",
      "source": "my-pipeline",
      "type": "dev.cdevents.service.deployed.0.2.0",
      "timestamp": "2024-01-15T10:30:00Z"
    },
    "subject": {
      "id": "my-service",
      "type": "service",
      "content": {
        "environment": {"id": "production"},
        "artifactId": "pkg:oci/my-service@v1.2.3"
      }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check your test consumer terminal - you should see the event appear immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Learn
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;CDEvent structure&lt;/strong&gt;: See exactly what fields are required&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;HTTP basics&lt;/strong&gt;: Understand headers and request format&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Quick iteration&lt;/strong&gt;: Test different event types rapidly&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Immediate validation&lt;/strong&gt;: See results in test consumer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;No security&lt;/strong&gt;: No authentication or request signing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Static IDs&lt;/strong&gt;: Hardcoded ID causes deduplication issues&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Not production-ready&lt;/strong&gt;: Missing security and proper ID generation&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;: Use basic curl to understand CDEvents structure, then graduate to more robust approaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: Production-Ready Bash Script
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: CI/CD integration with minimal dependencies (bash, curl, openssl, uuidgen), production pipelines&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perfect for&lt;/strong&gt;: Teams that need security but can't install cdviz-collector&lt;/p&gt;

&lt;h3&gt;
  
  
  Production-Ready Script Example
&lt;/h3&gt;

&lt;p&gt;This script demonstrates three critical production features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;HMAC-SHA256 signature&lt;/strong&gt; for request authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique ID&lt;/strong&gt; using UUID v7 (time-ordered)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current timestamp&lt;/strong&gt; for accurate event timing
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# send-cdevent.sh - Production-ready CDEvent sender&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="c"&gt;# Configuration (can be parameterized via environment variables)&lt;/span&gt;
&lt;span class="nv"&gt;API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;API_TOKEN&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;your&lt;/span&gt;&lt;span class="p"&gt;-secret-token-here&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;http&lt;/span&gt;://localhost:8080/webhook/000-cdevents&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Generate unique ID (UUID v7 - time-ordered)&lt;/span&gt;
&lt;span class="nv"&gt;EVENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen &lt;span class="nt"&gt;-7&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Create CDEvent payload&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; body.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "context": {
    "version": "0.4.1",
    "id": "&lt;/span&gt;&lt;span class="nv"&gt;$EVENT_ID&lt;/span&gt;&lt;span class="sh"&gt;",
    "source": "my-pipeline",
    "type": "dev.cdevents.service.deployed.0.2.0",
    "timestamp": "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"
  },
  "subject": {
    "id": "my-service",
    "type": "service",
    "content": {
      "environment": {"id": "production"},
      "artifactId": "pkg:oci/my-service@v1.2.3"
    }
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Generate HMAC-SHA256 signature&lt;/span&gt;
&lt;span class="nv"&gt;SIGNATURE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl dgst &lt;span class="nt"&gt;-hex&lt;/span&gt; &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-hmac&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; body.json&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;SIGNATURE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SIGNATURE&lt;/span&gt;&lt;span class="p"&gt;#* &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;  &lt;span class="c"&gt;# Remove "SHA256(filename)= " prefix&lt;/span&gt;

&lt;span class="c"&gt;# Send the CDEvent with signature&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WEBHOOK_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Signature: sha256=&lt;/span&gt;&lt;span class="nv"&gt;$SIGNATURE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; @body.json

&lt;span class="c"&gt;# Cleanup&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; body.json

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"CDEvent sent successfully with ID: &lt;/span&gt;&lt;span class="nv"&gt;$EVENT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to Use the Production Script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Basic usage with defaults&lt;/span&gt;
./send-cdevent.sh

&lt;span class="c"&gt;# Production usage with environment variables&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-production-secret"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://events.company.com/webhook"&lt;/span&gt;

./send-cdevent.sh

&lt;span class="c"&gt;# Parameterize the script further by editing hardcoded values&lt;/span&gt;
&lt;span class="c"&gt;# (service name, environment, artifact ID, etc.)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Understanding the Security Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. HMAC-SHA256 Signature&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;&lt;span class="nv"&gt;SIGNATURE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl dgst &lt;span class="nt"&gt;-hex&lt;/span&gt; &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-hmac&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; body.json&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Cryptographic proof that the sender has the secret token&lt;/li&gt;
&lt;li&gt;Prevents unauthorized event submission&lt;/li&gt;
&lt;li&gt;Verifiable by the receiver without sending the token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. UUID v7 for Unique IDs&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;&lt;span class="nv"&gt;EVENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen &lt;span class="nt"&gt;-7&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Time-ordered UUIDs (includes timestamp component)&lt;/li&gt;
&lt;li&gt;Guaranteed uniqueness without coordination&lt;/li&gt;
&lt;li&gt;Sortable by creation time&lt;/li&gt;
&lt;li&gt;No content-based ID chicken-and-egg problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Current Timestamp&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;&lt;span class="s2"&gt;"timestamp"&lt;/span&gt;: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;ISO 8601 format with UTC timezone&lt;/li&gt;
&lt;li&gt;Accurate event timing for correlation&lt;/li&gt;
&lt;li&gt;Prevents replay attacks when combined with signature&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Validating Signatures with cdviz-collector
&lt;/h3&gt;

&lt;p&gt;To test signature validation, update your test consumer configuration to verify the HMAC signature by adding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# cdviz-collector-debug.toml&lt;/span&gt;

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

&lt;span class="c"&gt;# Validate HMAC signature&lt;/span&gt;
&lt;span class="nn"&gt;[sources.cdevents_webhook.extractor.headers.x-signature]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"signature"&lt;/span&gt;
&lt;span class="py"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-secret-token-here"&lt;/span&gt;  &lt;span class="c"&gt;# Must match API_TOKEN in script&lt;/span&gt;
&lt;span class="py"&gt;algorithm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sha256"&lt;/span&gt;
&lt;span class="py"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"sha256&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart your test consumer with the updated configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdviz-collector connect &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--config&lt;/span&gt; ./cdviz-collector-debug.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when you run your script, the collector will &lt;strong&gt;Reject events&lt;/strong&gt; with invalid or missing signatures.&lt;/p&gt;

&lt;p&gt;Try sending with the wrong token to see rejection:&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;# This will be rejected&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"wrong-token"&lt;/span&gt;
./send-cdevent.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What You Learn
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Production security&lt;/strong&gt;: HMAC signatures for authentication&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Unique IDs&lt;/strong&gt;: Time-ordered UUIDs for guaranteed uniqueness&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Accurate timing&lt;/strong&gt;: Current timestamps in correct format&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Minimal dependencies&lt;/strong&gt;: Requires only bash, curl, openssl, uuidgen (common Unix tools)&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;CI/CD ready&lt;/strong&gt;: Easy to integrate into existing pipelines&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;No content-based deduplication&lt;/strong&gt;: UUIDs are always unique (unlike content-based CIDs)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Limited destinations&lt;/strong&gt;: Only sends to one HTTP endpoint&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Manual parameterization&lt;/strong&gt;: Must edit script or use environment variables&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;: Use production bash scripts when you can't install cdviz-collector but need production-grade security.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 3: cdviz-collector send - Recommended ⭐
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: Production deployments, multiple destinations, simplicity&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perfect for&lt;/strong&gt;: Teams wanting production features without custom scripting&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br&gt;
You already installed &lt;code&gt;cdviz-collector&lt;/code&gt; for the test consumer setup. If you need to install it on a different machine, see the &lt;strong&gt;&lt;a href="https://cdviz.dev/docs/cdviz-collector/install.html" rel="noopener noreferrer"&gt;complete installation guide&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 1: Send Your First Event (Simple)
&lt;/h3&gt;

&lt;p&gt;The simplest approach - let cdviz-collector generate IDs and timestamps:&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;# Minimal CDEvent - automatic ID and timestamp&lt;/span&gt;
cdviz-collector send &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
  "context": {
    "version": "0.4.1",
    "source": "my-pipeline",
    "type": "dev.cdevents.service.deployed.0.2.0"
  },
  "subject": {
    "id": "my-service",
    "type": "service",
    "content": {
      "environment": {"id": "production"},
      "artifactId": "pkg:oci/my-service@v1.2.3"
    }
  }
}'&lt;/span&gt; &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:8080/webhook/000-cdevents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What just happened&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Collector generated a content-based CID automatically&lt;/li&gt;
&lt;li&gt;✅ Collector added current timestamp automatically&lt;/li&gt;
&lt;li&gt;✅ Event validated against CDEvents specification&lt;/li&gt;
&lt;li&gt;✅ Sent to the webhook endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check your test consumer - the event appears with generated ID and timestamp!&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Add Authentication
&lt;/h3&gt;

&lt;p&gt;Create a configuration file for authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# send-config.toml&lt;/span&gt;
&lt;span class="nn"&gt;[sinks.http]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;destination&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://localhost:8080/webhook/000-cdevents"&lt;/span&gt;

&lt;span class="c"&gt;# Add HMAC signature (same as bash script)&lt;/span&gt;
&lt;span class="nn"&gt;[sinks.http.headers.x-signature]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"signature"&lt;/span&gt;
&lt;span class="py"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-secret-token-here"&lt;/span&gt;
&lt;span class="py"&gt;algorithm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sha256"&lt;/span&gt;
&lt;span class="py"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"sha256&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now send with authentication:&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;# Send with automatic signature generation&lt;/span&gt;
cdviz-collector send &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "context": {
      "version": "0.4.1",
      "source": "my-pipeline",
      "type": "dev.cdevents.service.deployed.0.2.0"
    },
    "subject": {
      "id": "my-service",
      "type": "service",
      "content": {
        "environment": {"id": "production"},
        "artifactId": "pkg:oci/my-service@v1.2.3"
      }
    }
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config&lt;/span&gt; send-config.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Production Deployment
&lt;/h3&gt;

&lt;p&gt;For production, use environment variables to override configuration values securely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# send-production.toml&lt;/span&gt;
&lt;span class="nn"&gt;[sinks.http]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;destination&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://events.company.com/webhook"&lt;/span&gt;

&lt;span class="c"&gt;# HMAC signature (token will be overridden by environment variable)&lt;/span&gt;
&lt;span class="nn"&gt;[sinks.http.headers.x-signature]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"signature"&lt;/span&gt;
&lt;span class="py"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"placeholder"&lt;/span&gt;  &lt;span class="c"&gt;# Will be overridden by CDVIZ_COLLECTOR__SINKS__HTTP__HEADERS__X_SIGNATURE__TOKEN&lt;/span&gt;
&lt;span class="py"&gt;algorithm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sha256"&lt;/span&gt;
&lt;span class="py"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"sha256&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="c"&gt;# Additional production headers (value will be overridden by environment variable)&lt;/span&gt;
&lt;span class="nn"&gt;[sinks.http.headers.authorization]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"secret"&lt;/span&gt;
&lt;span class="py"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"placeholder"&lt;/span&gt;  &lt;span class="c"&gt;# Will be overridden by CDVIZ_COLLECTOR__SINKS__HTTP__HEADERS__AUTHORIZATION__VALUE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy in production using environment variable overrides:&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;# Set secrets via environment variables that override TOML configuration&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CDVIZ_COLLECTOR__SINKS__HTTP__HEADERS__X_SIGNATURE__TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-production-secret"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CDVIZ_COLLECTOR__SINKS__HTTP__HEADERS__AUTHORIZATION__VALUE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Bearer your-api-token"&lt;/span&gt;

&lt;span class="c"&gt;# Send events in production&lt;/span&gt;
cdviz-collector send &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; @event.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config&lt;/span&gt; send-production.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
Configuration values can be overridden using environment variables with the pattern &lt;code&gt;CDVIZ_COLLECTOR__SECTION__SUBSECTION__KEY&lt;/code&gt;. This allows keeping secrets out of configuration files. See &lt;strong&gt;&lt;a href="https://cdviz.dev/docs/cdviz-collector/configuration.html#environment-overrides" rel="noopener noreferrer"&gt;Environment Overrides documentation&lt;/a&gt;&lt;/strong&gt; for complete details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What You Learn
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Simplicity&lt;/strong&gt;: Minimal JSON, automatic ID/timestamp generation&lt;br&gt;
✅ &lt;strong&gt;Production security&lt;/strong&gt;: Built-in authentication without scripting&lt;br&gt;
✅ &lt;strong&gt;Configuration-based&lt;/strong&gt;: Secrets in config files, not code&lt;br&gt;
✅ &lt;strong&gt;Format validation&lt;/strong&gt;: Built-in CDEvent validation&lt;br&gt;
✅ &lt;strong&gt;Flexibility&lt;/strong&gt;: Easy to change destinations without code changes&lt;br&gt;
✅ &lt;strong&gt;Best practices&lt;/strong&gt;: Follows CDEvents recommendations automatically&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;External dependency&lt;/strong&gt;: Requires installing cdviz-collector&lt;br&gt;
❌ &lt;strong&gt;Learning curve&lt;/strong&gt;: Need to understand configuration format&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;: Use &lt;code&gt;cdviz-collector send&lt;/code&gt; for all production deployments. It's simpler, more secure, and more flexible than custom scripts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br&gt;
&lt;strong&gt;Beyond HTTP&lt;/strong&gt;: &lt;code&gt;cdviz-collector send&lt;/code&gt; can send to any sink type - just Kafka, just Database, just S3, or any combination of multiple destinations simultaneously. One command can target HTTP + Database, multiple Kafka topics, or any mix of available sinks. See the &lt;strong&gt;&lt;a href="https://cdviz.dev/docs/cdviz-collector/sinks/" rel="noopener noreferrer"&gt;complete sinks documentation&lt;/a&gt;&lt;/strong&gt; for all sink types and configuration examples.&lt;br&gt;
Let us know if you'd like to see examples in future episodes!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Choosing Your Sending Approach
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Decision Framework
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Start with basic curl if&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're learning CDEvents and want to understand the structure&lt;/li&gt;
&lt;li&gt;You need quick experimentation without setup&lt;/li&gt;
&lt;li&gt;You're prototyping and testing locally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use production bash script if&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need production security (HMAC signatures)&lt;/li&gt;
&lt;li&gt;You can't install external dependencies in your CI/CD&lt;/li&gt;
&lt;li&gt;You only need to send to one HTTP endpoint&lt;/li&gt;
&lt;li&gt;You're comfortable maintaining bash scripts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use cdviz-collector send if&lt;/strong&gt; (⭐ Recommended):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want production-ready features without custom scripting&lt;/li&gt;
&lt;li&gt;You need to send to multiple destinations&lt;/li&gt;
&lt;li&gt;You want automatic ID and timestamp generation&lt;/li&gt;
&lt;li&gt;You prefer configuration over code&lt;/li&gt;
&lt;li&gt;You're building for long-term maintainability&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Progression Strategy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Recommended learning path&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Basic curl&lt;/strong&gt; (5 minutes) → Understand CDEvent structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with cdviz-collector connect&lt;/strong&gt; (5 minutes) → Validate your events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cdviz-collector send&lt;/strong&gt; (10 minutes) → Production implementation ⭐&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For most teams, &lt;strong&gt;cdviz-collector send&lt;/strong&gt; provides the best balance of simplicity, security, and flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;🎯 &lt;strong&gt;Start simple&lt;/strong&gt;: Begin with curl to understand CDEvents structure&lt;br&gt;
🔒 &lt;strong&gt;Add security&lt;/strong&gt;: Production requires HMAC signatures and proper IDs&lt;br&gt;
⭐ &lt;strong&gt;Use the right tool&lt;/strong&gt;: cdviz-collector send for production deployments&lt;br&gt;
📊 &lt;strong&gt;Test locally&lt;/strong&gt;: Use cdviz-collector connect to validate events&lt;br&gt;
🚀 &lt;strong&gt;Multiple destinations&lt;/strong&gt;: Send to HTTP, Database, Kafka, S3 simultaneously&lt;br&gt;
📈 &lt;strong&gt;Follow best practices&lt;/strong&gt;: Let tools generate IDs and timestamps automatically&lt;/p&gt;

&lt;p&gt;Understanding how to send CDEvents properly is essential for SDLC observability. These three approaches give you options for every scenario, from learning to production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Try these experiments&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send a basic curl event to your local test consumer&lt;/li&gt;
&lt;li&gt;Enhance the bash script with your own authentication&lt;/li&gt;
&lt;li&gt;Install cdviz-collector and send with automatic ID generation&lt;/li&gt;
&lt;li&gt;Configure multiple destinations in a single send command&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the next episode, we'll explore &lt;strong&gt;GitHub Actions integration&lt;/strong&gt; - how to automatically send CDEvents from your GitHub workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CDviz Documentation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cdviz.dev/docs/cdviz-collector/send.html" rel="noopener noreferrer"&gt;cdviz-collector send documentation&lt;/a&gt; - Complete command reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cdviz.dev/docs/cdviz-collector/sinks/http.html" rel="noopener noreferrer"&gt;HTTP Sink documentation&lt;/a&gt; - Authentication patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./episode-1-simulate-consumer.md"&gt;Episode #1: Simulate a Consumer&lt;/a&gt; - Learn to test event reception&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Alternative Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://brunseba.github.io/cdevents-tools/" rel="noopener noreferrer"&gt;cdevents-tools CLI&lt;/a&gt; - CDEvents-native CLI for generation and sending&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cdevents.dev" rel="noopener noreferrer"&gt;CDEvents Specification&lt;/a&gt; - Complete event standard reference&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cdevent</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>CDEvents in Action #1: Simulate a Consumer</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Tue, 16 Sep 2025 19:18:33 +0000</pubDate>
      <link>https://dev.to/davidb31/cdevents-in-action-1-simulate-a-consumer-aip</link>
      <guid>https://dev.to/davidb31/cdevents-in-action-1-simulate-a-consumer-aip</guid>
      <description>&lt;p&gt;&lt;em&gt;Before you build your CDEvents integration, test your approach. Three methods help you understand event flows and validate your strategy before writing production code.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Testing Problem
&lt;/h2&gt;

&lt;p&gt;You understand why CDEvents matter for pipeline visibility. Now you want to integrate, but you're facing the classic integration challenge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How do I know my events will work?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What should I expect to receive?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How do I test without building everything first?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building an event consumer before understanding the data flow is like writing SQL queries before seeing the database schema. You need to &lt;strong&gt;simulate receiving events first&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Approaches to Simulate CDEvents Consumption
&lt;/h2&gt;

&lt;p&gt;Each approach serves different needs and skill levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Setup Time&lt;/th&gt;
&lt;th&gt;Real Data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;webhook.site&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quick testing, event structure exploration&lt;/td&gt;
&lt;td&gt;30 seconds&lt;/td&gt;
&lt;td&gt;❌ Manual only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CDviz docker compose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full pipeline simulation, realistic testing&lt;/td&gt;
&lt;td&gt;3 minutes&lt;/td&gt;
&lt;td&gt;✅ Demo events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;cdviz-collector connect&lt;/strong&gt; ⭐&lt;/td&gt;
&lt;td&gt;Local development, production debugging&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;td&gt;✅ Real events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's explore each approach and learn when to use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: webhook.site - Quick Event Structure Testing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: Exploring CDEvents structure, quick prototyping, learning event schemas&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perfect for&lt;/strong&gt;: Developers new to CDEvents who want to see event structure immediately&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Get Your Unique Webhook URL
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://webhook.site" rel="noopener noreferrer"&gt;webhook.site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Copy your unique URL (looks like &lt;code&gt;https://webhook.site/12345678-abcd-...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Leave the tab open to see incoming requests&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: Send a Test CDEvent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Send a deployment event to your webhook.site URL&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://webhook.site/YOUR-UNIQUE-ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "context": {
      "version": "0.4.1",
      "id": "test-123",
      "source": "my-pipeline",
      "type": "dev.cdevents.service.deployed.0.2.0",
      "timestamp": "'&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s1"&gt;'"
    },
    "subject": {
      "id": "my-service",
      "type": "service",
      "content": {
        "environment": {"id": "production"},
        "artifactId": "pkg:oci/my-service@v1.2.3"
      }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Analyze the Event Structure
&lt;/h3&gt;

&lt;p&gt;Switch back to webhook.site to see your event. Notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headers&lt;/strong&gt;: &lt;code&gt;Content-Type&lt;/code&gt;, user agent, IP address&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body&lt;/strong&gt;: Complete CDEvent structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing&lt;/strong&gt;: When the event was received&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size&lt;/strong&gt;: Event payload size&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What You Learn
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Event structure&lt;/strong&gt;: See exactly what CDEvents look like&lt;br&gt;
✅ &lt;strong&gt;Header requirements&lt;/strong&gt;: Understand HTTP headers needed&lt;br&gt;
✅ &lt;strong&gt;JSON schema&lt;/strong&gt;: Valid CDEvent format and required fields&lt;br&gt;
✅ &lt;strong&gt;Quick iteration&lt;/strong&gt;: Test different event types rapidly&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;No processing&lt;/strong&gt;: Events just display, no storage or correlation&lt;br&gt;
❌ &lt;strong&gt;Manual only&lt;/strong&gt;: You must send events yourself&lt;br&gt;
❌ &lt;strong&gt;No authentication&lt;/strong&gt;: Simple HTTP POST only&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;: Use webhook.site to understand CDEvents structure before building your consumer logic.&lt;/p&gt;
&lt;h2&gt;
  
  
  Approach 2: CDviz docker compose - Realistic Pipeline Testing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: Testing complete event flows, understanding CDviz integration, realistic event scenarios&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perfect for&lt;/strong&gt;: Teams evaluating CDviz, developers building CDEvents integration, demonstrating event flows with visual dashboards&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Start CDviz Demo Environment
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone and start the full CDviz stack&lt;/span&gt;
git clone https://github.com/cdviz-dev/cdviz.git
&lt;span class="nb"&gt;cd &lt;/span&gt;cdviz/demos/stack-compose
docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;What starts up&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CDviz collector (event ingestion)&lt;/li&gt;
&lt;li&gt;PostgreSQL + TimescaleDB (event storage)&lt;/li&gt;
&lt;li&gt;Grafana (visualization dashboards)&lt;/li&gt;
&lt;li&gt;Demo event generator (realistic test data)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 2: Observe Automatic Demo Events
&lt;/h3&gt;

&lt;p&gt;Open &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;localhost:3000&lt;/a&gt; for Grafana dashboards. You'll immediately see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service deployments&lt;/strong&gt; across different environments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline executions&lt;/strong&gt; with success/failure patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact promotions&lt;/strong&gt; through staging to production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeline visualization&lt;/strong&gt; showing event correlations&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 3: Send Your Own Events
&lt;/h3&gt;

&lt;p&gt;The CDviz collector accepts events at &lt;code&gt;http://localhost:8080/webhook/000-cdevents&lt;/code&gt;:&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;# Send a custom deployment event&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/webhook/000-cdevents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "context": {
      "version": "0.4.1",
      "id": "my-test-deployment",
      "source": "my-testing",
      "type": "dev.cdevents.service.deployed.0.2.0",
      "timestamp": "'&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s1"&gt;'"
    },
    "subject": {
      "id": "my-namespace/my-test-service/my-container",
      "type": "service",
      "content": {
        "environment": {"id": "testing"},
        "artifactId": "pkg:oci/my-test-service@v2.1.0"
      }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Your event appears in Grafana dashboards alongside demo events, showing realistic integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Explore Event Correlation
&lt;/h3&gt;

&lt;p&gt;Browse different Grafana dashboards to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service Timeline&lt;/strong&gt;: How your events fit into deployment flows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment View&lt;/strong&gt;: Cross-environment event correlation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activity Feed&lt;/strong&gt;: Chronological event stream&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct Database&lt;/strong&gt;: Query PostgreSQL directly for custom analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What You Learn
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Complete integration&lt;/strong&gt;: Full event processing pipeline&lt;br&gt;
✅ &lt;strong&gt;Visual feedback&lt;/strong&gt;: See events in realistic dashboards&lt;br&gt;
✅ &lt;strong&gt;Event correlation&lt;/strong&gt;: Understand how events relate to each other&lt;br&gt;
✅ &lt;strong&gt;Storage patterns&lt;/strong&gt;: Events stored in PostgreSQL for analysis&lt;br&gt;
✅ &lt;strong&gt;Production simulation&lt;/strong&gt;: Realistic event processing behavior&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Resource intensive&lt;/strong&gt;: Requires Docker and multiple containers&lt;br&gt;
❌ &lt;strong&gt;Complex setup&lt;/strong&gt;: More moving parts than simple webhook testing&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;: Use CDviz docker compose when evaluating CDEvents for your team or testing integration patterns.&lt;/p&gt;
&lt;h2&gt;
  
  
  Approach 3: cdviz-collector connect - Recommended for Development ⭐
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: Local development, troubleshooting integrations, validating event flows&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perfect for&lt;/strong&gt;: Developers building CDEvents integration, DevOps engineers debugging event flows&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Install cdviz-collector CLI
&lt;/h3&gt;

&lt;p&gt;All installation options are documented at &lt;a href="https://cdviz.dev/docs/cdviz-collector/install.html" rel="noopener noreferrer"&gt;CDviz Collector Installation Guide&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;cdviz-dev/tap/cdviz-collector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If native pre-built binary is not available for your platform (e.g. Windows) you can fallback to the docker image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Launch with a local debug configuration
&lt;/h3&gt;

&lt;p&gt;Create a configuration with a webhook as source (input) and a debug sink (output)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# cdviz-collector-debug.toml&lt;/span&gt;

&lt;span class="nn"&gt;[http]&lt;/span&gt;
&lt;span class="py"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.0.0.0"&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;

&lt;span class="nn"&gt;[sinks.debug]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"debug"&lt;/span&gt;
&lt;span class="py"&gt;format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"json"&lt;/span&gt;
&lt;span class="py"&gt;destination&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"stdout"&lt;/span&gt;

&lt;span class="nn"&gt;[sources.cdevents_webhook]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[sources.cdevents_webhook.extractor]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"webhook"&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"000-cdevents"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdviz-collector connect &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--config&lt;/span&gt; ./cdviz-collector-debug.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starts a local CDviz collector instance&lt;/li&gt;
&lt;li&gt;Shows real-time events as they arrive on stdout&lt;/li&gt;
&lt;li&gt;Validates CDEvent format and rejects invalid events&lt;/li&gt;
&lt;li&gt;Provides a lightweight development environment without Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Send Your Own Events
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Send a custom deployment event&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/webhook/000-cdevents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "context": {
      "version": "0.4.1",
      "id": "my-test-deployment",
      "source": "my-testing",
      "type": "dev.cdevents.service.deployed.0.2.0",
      "timestamp": "'&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s1"&gt;'"
    },
    "subject": {
      "id": "my-namespace/my-test-service/my-container",
      "type": "service",
      "content": {
        "environment": {"id": "testing"},
        "artifactId": "pkg:oci/my-test-service@v2.1.0"
      }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What You Learn
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;CDEvent validation&lt;/strong&gt;: Immediate feedback on event format and structure&lt;br&gt;
✅ &lt;strong&gt;Lightweight development&lt;/strong&gt;: No Docker required, fast startup&lt;br&gt;
✅ &lt;strong&gt;Real-time debugging&lt;/strong&gt;: See events as they arrive with validation feedback&lt;br&gt;
✅ &lt;strong&gt;Configuration flexibility&lt;/strong&gt;: Easy to modify behavior via TOML config&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Command-line only&lt;/strong&gt;: No visual interface like Grafana&lt;br&gt;
❌ &lt;strong&gt;Local only&lt;/strong&gt;: Events not persisted or aggregated&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;: Use cdviz-collector connect as your primary development tool for CDEvents integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing Your Testing Approach
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Decision Framework
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Start with webhook.site if&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're new to CDEvents and want to understand event structure&lt;/li&gt;
&lt;li&gt;You need quick experimentation with different event formats&lt;/li&gt;
&lt;li&gt;You're prototyping and don't need full pipeline simulation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use CDviz docker compose if&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're evaluating CDviz for your organization&lt;/li&gt;
&lt;li&gt;You want to see realistic event flows and correlations&lt;/li&gt;
&lt;li&gt;You need to test integration patterns before production deployment&lt;/li&gt;
&lt;li&gt;You want to explore Grafana dashboards and event visualization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use cdviz-collector connect if&lt;/strong&gt; (⭐ Recommended):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're developing CDEvents integration locally&lt;/li&gt;
&lt;li&gt;You want fast feedback on event validation&lt;/li&gt;
&lt;li&gt;You prefer lightweight tools without Docker overhead&lt;/li&gt;
&lt;li&gt;You need flexible configuration for different scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Progression Strategy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Recommended learning path&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;webhook.site&lt;/strong&gt; (5 minutes) → Understand CDEvent structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cdviz-collector connect&lt;/strong&gt; (10 minutes) → Validate and debug locally ⭐&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDviz docker compose&lt;/strong&gt; (15 minutes) → See complete integration with dashboards&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For most developers, &lt;strong&gt;cdviz-collector connect&lt;/strong&gt; provides the best balance of simplicity and functionality for ongoing development work.&lt;/p&gt;

&lt;p&gt;This progression gives you theoretical knowledge, practical experience, and production debugging skills.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps: From Consumer to Producer
&lt;/h2&gt;

&lt;p&gt;Now that you understand how to receive and inspect CDEvents, you're ready to learn how to &lt;strong&gt;send&lt;/strong&gt; them from your own tools and pipelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try these experiments&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick one simulation approach above and test it&lt;/li&gt;
&lt;li&gt;Send different CDEvent types (&lt;code&gt;service.deployed&lt;/code&gt;, &lt;code&gt;taskrun.finished&lt;/code&gt;, &lt;code&gt;artifact.published&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Note what you learned about event structure and timing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This foundation will help you build robust CDEvents integration in your own systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;🎯 &lt;strong&gt;Test first&lt;/strong&gt;: Simulate receiving events before building producers&lt;br&gt;
🔧 &lt;strong&gt;Multiple tools&lt;/strong&gt;: Different approaches serve different testing needs&lt;br&gt;
⭐ &lt;strong&gt;Recommended approach&lt;/strong&gt;: cdviz-collector connect for most development work&lt;br&gt;
📊 &lt;strong&gt;Visual integration&lt;/strong&gt;: CDviz docker compose for complete pipeline visualization&lt;br&gt;
📈 &lt;strong&gt;Progressive learning&lt;/strong&gt;: Start simple (webhook.site) then build practical skills&lt;/p&gt;

&lt;p&gt;Understanding how to consume CDEvents is essential before producing them. These simulation approaches give you the foundation to build robust CDEvents integration.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://webhook.site" rel="noopener noreferrer"&gt;webhook.site&lt;/a&gt; - Instant webhook testing&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/cdviz-dev/cdviz/tree/main/demos/stack-compose" rel="noopener noreferrer"&gt;CDviz Demo Setup&lt;/a&gt; - Full docker compose stack&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cdevents.dev" rel="noopener noreferrer"&gt;CDEvents Specification&lt;/a&gt; - Complete event standard reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cdviz.dev" rel="noopener noreferrer"&gt;CDviz Documentation&lt;/a&gt; - Complete installation and configuration guides&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cicd</category>
      <category>cdevent</category>
      <category>webhook</category>
      <category>devops</category>
    </item>
    <item>
      <title>CDEvents in Action #0: Monitor Your Software Factory</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Thu, 21 Aug 2025 11:12:53 +0000</pubDate>
      <link>https://dev.to/davidb31/pipeline-visibility-crisis-when-your-tools-dont-talk-3ch</link>
      <guid>https://dev.to/davidb31/pipeline-visibility-crisis-when-your-tools-dont-talk-3ch</guid>
      <description>&lt;p&gt;&lt;em&gt;You have an impressive CI/CD stack: GitHub, Jenkins, Kubernetes, Datadog, PagerDuty. But when leadership asks "How fast do we deploy?" you're opening 6&lt;br&gt;
  browser tabs and building spreadsheets.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Current Reality
&lt;/h2&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%2F4l4v3f2ox4ntk55yyl5j.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%2F4l4v3f2ox4ntk55yyl5j.png" alt="current cicd stack" width="493" height="792"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before CDviz: You manually correlate data from scattered tools&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each tool has its own data, but few tools communicate with each other. You become the integration layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CDviz Actually Provides Today
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Event Collection&lt;/strong&gt;: Collects events (CDEvents, native HTTP events, or states) and produces standardized &lt;a href="https://cdevents.dev/" rel="noopener noreferrer"&gt;CDEvents&lt;/a&gt; from your existing tools&lt;br&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: PostgreSQL database with TimescaleDB extension for time-series CDEvent storage&lt;br&gt;
&lt;strong&gt;Visualization&lt;/strong&gt;: Pre-built Grafana dashboards for deployment tracking and execution metrics&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%2Fx1tw8rk3frlum15cuhb7.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%2Fx1tw8rk3frlum15cuhb7.png" alt="cdviz and the cicd stack" width="496" height="862"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Current Dashboards
&lt;/h3&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%2F0b57owpnvccmhcss6431.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%2F0b57owpnvccmhcss6431.png" alt="description of the artifact timeline panel" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Service and artifact deployment timeline across stages (registry, environments, etc.)&lt;/li&gt;
&lt;li&gt;CDEvents activity feed with real-time updates&lt;/li&gt;
&lt;li&gt;Duration metrics for tasks, pipelines, and tests&lt;/li&gt;
&lt;li&gt;Queries to annotate runtime metrics with version information&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What's Not Included (Yet)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;DORA metrics calculations (you build the SQL queries)&lt;/li&gt;
&lt;li&gt;Automated incident correlation (data is available, custom queries required)&lt;/li&gt;
&lt;li&gt;Multi-team comparisons (possible but requires additional configuration)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5-Minute Demo: See the Difference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start CDviz locally&lt;/span&gt;
git clone https://github.com/cdviz-dev/cdviz.git
&lt;span class="nb"&gt;cd &lt;/span&gt;cdviz/demos/stack-compose
docker compose up

&lt;span class="c"&gt;# Send a deployment event&lt;/span&gt;
curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/webhook/000-cdevents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "context": {
      "version": "0.4.1",
      "id": "0",
      "source": "demo",
      "type": "dev.cdevents.service.deployed.0.2.0",
      "timestamp": "'&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s1"&gt;'"
    },
    "subject": {
      "id": "my-namespace/my-service/my-container",
      "type": "service",
      "content": {
        "environment": {
          "id": "my-group/my-region"
        },
        "artifactId": "pkg:oci/my-service@v1.2.3"
      }
    }
  }'&lt;/span&gt;

&lt;span class="c"&gt;# View dashboard&lt;/span&gt;
open localhost:3000/d/demo_service_deployed/service3a-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Event appears immediately in timeline. No manual correlation needed.&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%2Fi2ht0bhke5qboe1a6w2b.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%2Fi2ht0bhke5qboe1a6w2b.png" alt="sample data with annotation" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some events are generated automatically by the demo. Browse the other dashboard to see some possibilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Value: Time and Accuracy
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before CDviz&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query GitHub: 5 minutes&lt;/li&gt;
&lt;li&gt;Check Jenkins: 3 minutes&lt;/li&gt;
&lt;li&gt;Correlate timestamps: 10 minutes&lt;/li&gt;
&lt;li&gt;Build report: 15 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total&lt;/strong&gt;: 33 minutes per request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;After CDviz integration&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open dashboard: 1 minute&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total&lt;/strong&gt;: 1 minute&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real Questions CDviz Helps Answer
&lt;/h2&gt;

&lt;p&gt;✅ "What version is running in production right now?"&lt;br&gt;
✅ "When did we last deploy service X?"&lt;br&gt;
✅ "Show me all deployments from last week"&lt;br&gt;
✅ "What's our deployment frequency?"&lt;/p&gt;

&lt;p&gt;🔄 "How do teams compare?" (data available, dashboard TBD)&lt;br&gt;
❌ "What's our lead time?" (needs commit events integration)&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;You can look at &lt;a href="https://cdviz.dev" rel="noopener noreferrer"&gt;CDviz's documentation&lt;/a&gt; until the coming articles about how to instrument and generate CDEvents, how to improve communication between tools via CDEvents + CDviz.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>observability</category>
      <category>sdlc</category>
    </item>
    <item>
      <title>Add a friendly HTTP polling to your API with Retry-After</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Sun, 15 May 2022 17:26:19 +0000</pubDate>
      <link>https://dev.to/davidb31/add-a-friendly-http-polling-to-your-api-3jle</link>
      <guid>https://dev.to/davidb31/add-a-friendly-http-polling-to-your-api-3jle</guid>
      <description>&lt;p&gt;Polling is a way to handle long, delayed, queued, asynchron work without blocking a TCP connection. To do (long) polling on http server we need at least 2 endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The endpoint to start the work (eg: &lt;code&gt;POST /start_work&lt;/code&gt; or &lt;code&gt;POST /work&lt;/code&gt;for REST like api)&lt;/li&gt;
&lt;li&gt;The endpoint to provide the result of the work when ready, or a "not ready yet" status to tell "retry later" (eg &lt;code&gt;GET /work/{work_id}&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JRgocG4V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u7330p6519imggxcspod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JRgocG4V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u7330p6519imggxcspod.png" alt="sequence diagram based on body content" width="857" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This approach implies a &lt;strong&gt;per endpoint&lt;/strong&gt; logic :-( , handled by the Caller!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to read &lt;code&gt;work_id&lt;/code&gt; from the result of &lt;code&gt;POST /start_work&lt;/code&gt; ? Is the status-code is 200 or 202 ?&lt;/li&gt;
&lt;li&gt;How to convert &lt;code&gt;work_id&lt;/code&gt; into url request for &lt;code&gt;GET /work/{work_id}&lt;/code&gt; and handle the response (ready vs not ready) ?&lt;/li&gt;
&lt;li&gt;What is the retry interval? Is it defined in the documentation, as an arbitrary value or as optional info in the response ?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Concept Evolution: use http redirect &amp;amp; retry-after
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The server provides not the interval but an estimation for when to try next time via &lt;code&gt;Retry-After&lt;/code&gt; http attribute (case insensitive).&lt;/li&gt;
&lt;li&gt;The server provides the endpoint to get the result via the 303 &lt;code&gt;SEE_OTHER&lt;/code&gt; status code and the &lt;code&gt;Location&lt;/code&gt; http attribute (and indirectly triggers the retry).&lt;/li&gt;
&lt;li&gt;The information are provided via http status code &amp;amp; attributes like handling of authentication, trace, circuit breaker, rate-limit...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So on caller side, the logic can be handled in a endpoint agnostic way (eg at the user-agent wrapper level), and reuse for every endpoint that use polling. &lt;/p&gt;

&lt;p&gt;On server side, switching between polling and direct response is transparent for the caller.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zipGd5oW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gnefl6eb2h3f3757pnyv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zipGd5oW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gnefl6eb2h3f3757pnyv.png" alt="sequence diagram with using status &amp;amp; header" width="880" height="690"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Pros
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;server can adjust &lt;code&gt;Retry-After&lt;/code&gt;, with estimation based on current load, progress of the work,...&lt;/li&gt;
&lt;li&gt;server can adjust the location of the response maybe to add complementary query parameters,...&lt;/li&gt;
&lt;li&gt;the protocol becomes is agnostic of the endpoint (may could become a "standard")&lt;/li&gt;
&lt;li&gt;the caller &amp;amp; user-agent are free to handle the polling as they want, it could like in the first example (with more information) or with a more complex way with queue intermediate state, via sidecar or proxy...

&lt;ul&gt;
&lt;li&gt;user-agent is free to follow redirect automatically or not, and to handle them as a blocking or non-blocking way&lt;/li&gt;
&lt;li&gt;user-agent handle retry-after like retries on&lt;/li&gt;
&lt;li&gt;rate-limit: 429 (Too Many Request) + Retry-After&lt;/li&gt;
&lt;li&gt;downtime: 503  (Service Unavailable) + Retry-After&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;work_id&lt;/code&gt; &amp;amp; polling can be (nearly) hide to the Caller, it's like a regular POST request that return the response&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ❌ Cons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the Caller should handle response of &lt;code&gt;GET /work/{work_id}&lt;/code&gt; as response of &lt;code&gt;POST /start_work&lt;/code&gt; (both possible error,...)&lt;/li&gt;
&lt;li&gt;often the implementation of user agent about following redirect, should be changed or handled by the wrapper

&lt;ul&gt;
&lt;li&gt;the user-agent should change the method from POST to GET on redirection (allowed for 301 (Move Permanently), 302 (Found), 303 (See Other)), this behavior can be coded at the user-agent wrapper level.&lt;/li&gt;
&lt;li&gt;some user-agent don't handle &lt;code&gt;Retry-After&lt;/code&gt; (remember http header are case insensitive)&lt;/li&gt;
&lt;li&gt;Some user-agent have a maximum number of redirect (eg with curl &lt;code&gt;Maximum (50) redirects followed&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;p&gt;Extracted from &lt;a href="https://datatracker.ietf.org/doc/html/rfc7231#section-6.4.4"&gt;RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content&lt;/a&gt; similar info available at M&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;303 See Other&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;... This status code is applicable to any HTTP method.  It is primarily used to allow the output of a POST action to redirect the user agent to a selected resource, since doing so provides the information corresponding to the POST response in a form that can be separately identified, bookmarked, and cached, independent of the original request. ...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Retry-After&lt;/code&gt;
&amp;gt; ... When sent with any 3xx (Redirection) response, Retry-After indicates the minimum time that the user agent is asked to wait before issuing the redirected request. ...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Extracted from &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After"&gt;Retry-After - HTTP | MDN&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Retry-After response HTTP header indicates how long the user agent should wait before making a follow-up request. There are three main cases this header is used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When sent with a 503 (Service Unavailable) response, this indicates how long the service is expected to be unavailable.&lt;/li&gt;
&lt;li&gt;When sent with a 429 (Too Many Requests) response, this indicates how long to wait before making a new request.&lt;/li&gt;
&lt;li&gt;When sent with a redirect response, such as 301 (Moved Permanently), this indicates the minimum time that the user agent is asked to wait before issuing the redirected request.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Implementations (aka PoC)
&lt;/h2&gt;

&lt;p&gt;🚧&lt;br&gt;
WARNING: the code is not optimal, and lot of improvements can be done. PR &amp;amp; feedbacks are welcomed (improvements, implementation for other clients,...)&lt;br&gt;
🚧&lt;/p&gt;
&lt;h3&gt;
  
  
  A basic server
&lt;/h3&gt;

&lt;p&gt;For the PoC, I created a basic http service in Rust. The code is available at &lt;a href="https://github.com/davidB/sandbox_http/tree/development/polling/server-axum"&gt;sandbox_http/polling/server-axum at development · davidB/sandbox_http&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;start_work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Extension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;works&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Extension&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WorkDb&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;IntoResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;StdRng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;SeedableRng&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_entropy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;work_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_v4&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="nf"&gt;.gen_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;end_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;get_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/work/{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;work_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;next_try&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="nf"&gt;.as_secs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;works&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;works&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"acquire works lock to start_work"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;works&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;work_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Work&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;work_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;end_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;nb_get_call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SEE_OTHER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;http&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;header&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;LOCATION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;http&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;header&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RETRY_AFTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_try&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Extension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;works&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Extension&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WorkDb&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;IntoResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;works&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;works&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"acquire works lock to get_work"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="n"&gt;work_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"request work result"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;works&lt;/span&gt;&lt;span class="nf"&gt;.get_mut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;work_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NOT_FOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.into_response&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="py"&gt;.end_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="py"&gt;.nb_get_call&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;get_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/work/{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="py"&gt;.work_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;next_try&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SEE_OTHER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;http&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;header&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;LOCATION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;http&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;header&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RETRY_AFTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_try&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                    &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nf"&gt;.into_response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="nf"&gt;.into_response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Do not forgot to protect the endpoint (like others) with a kind of rate-limit&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Caller with curl
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:8080/start_work"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Do not use &lt;code&gt;-X POST&lt;/code&gt; but &lt;code&gt;-d ""&lt;/code&gt; else redirection 303 does not switch from &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;GET&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Failed because curl doesn't support &lt;code&gt;Retry-After&lt;/code&gt; when follow redirection (see date in the sample below).&lt;/li&gt;
&lt;li&gt;Loop stops due to "maximum redirect", without this security overload on client and server.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
&amp;gt; POST /start_work HTTP/1.1
&amp;gt; Host: localhost:8080
&amp;gt; User-Agent: curl/7.82.0
&amp;gt; Accept: */*
&amp;gt; Content-Length: 0
&amp;gt; Content-Type: application/x-www-form-urlencoded
&amp;gt; 
* Mark bundle as not supporting multiuse
&amp;lt; HTTP/1.1 303 See Other
&amp;lt; location: /work/20913b17-1df3-40ed-b26a-df50414ecc1c
&amp;lt; retry-after: 8
&amp;lt; access-control-allow-origin: *
&amp;lt; vary: origin
&amp;lt; vary: access-control-request-method
&amp;lt; vary: access-control-request-headers
&amp;lt; content-length: 0
&amp;lt; date: Sun, 15 May 2022 13:07:56 GMT
&amp;lt; 
* Connection #0 to host localhost left intact
* Issue another request to this URL: 'http://localhost:8080/work/20913b17-1df3-40ed-b26a-df50414ecc1c'
* Switch to GET
* Found bundle for host localhost: 0x5586add25af0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) with host localhost
* Connected to localhost (127.0.0.1) port 8080 (#0)
&amp;gt; GET /work/20913b17-1df3-40ed-b26a-df50414ecc1c HTTP/1.1
&amp;gt; Host: localhost:8080
&amp;gt; User-Agent: curl/7.82.0
&amp;gt; Accept: */*
&amp;gt; 
* Mark bundle as not supporting multiuse
&amp;lt; HTTP/1.1 303 See Other
&amp;lt; location: /work/20913b17-1df3-40ed-b26a-df50414ecc1c
&amp;lt; retry-after: 1
&amp;lt; access-control-allow-origin: *
&amp;lt; vary: origin
&amp;lt; vary: access-control-request-method
&amp;lt; vary: access-control-request-headers
&amp;lt; content-length: 0
&amp;lt; date: Sun, 15 May 2022 13:07:56 GMT
&amp;lt; 

...

* Connection #0 to host localhost left intact
* Issue another request to this URL: 'http://localhost:8080/work/20913b17-1df3-40ed-b26a-df50414ecc1c'
* Found bundle for host localhost: 0x5586add25af0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) with host localhost
* Connected to localhost (127.0.0.1) port 8080 (#0)
&amp;gt; GET /work/20913b17-1df3-40ed-b26a-df50414ecc1c HTTP/1.1
&amp;gt; Host: localhost:8080
&amp;gt; User-Agent: curl/7.82.0
&amp;gt; Accept: */*
&amp;gt; 
* Mark bundle as not supporting multiuse
&amp;lt; HTTP/1.1 303 See Other
&amp;lt; location: /work/20913b17-1df3-40ed-b26a-df50414ecc1c
&amp;lt; retry-after: 1
&amp;lt; access-control-allow-origin: *
&amp;lt; vary: origin
&amp;lt; vary: access-control-request-method
&amp;lt; vary: access-control-request-headers
&amp;lt; content-length: 0
&amp;lt; date: Sun, 15 May 2022 13:07:56 GMT
&amp;lt; 
* Connection #0 to host localhost left intact
* Maximum (50) redirects followed
curl: (47) Maximum (50) redirects followed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Caller with your favorite programming language
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;There is lot chance that the &lt;code&gt;Retry-After&lt;/code&gt; is not well supported by your http client / user-agent. So

&lt;ul&gt;
&lt;li&gt;Test it, or notify your API consumers &amp;amp; customers to test it&lt;/li&gt;
&lt;li&gt;Open a ticket/issue to the project to request support or make a PR&lt;/li&gt;
&lt;li&gt;provide a workaround solution (until official support):&lt;/li&gt;
&lt;li&gt;Disable the default follow redirection,&lt;/li&gt;
&lt;li&gt;Implement follow redirect with support of &lt;code&gt;Retry-After&lt;/code&gt; into your wrapper&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;The way to handle delay can be shared with retry for "rate-limit", "downtime", "circuit-breaker"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As demonstration purpose I make a sample with &lt;a href=""&gt;reqwest&lt;/a&gt; one of the most used http client in Rust.&lt;br&gt;
You can look at it at &lt;a href="https://github.com/davidB/sandbox_http/blob/development/polling/server-axum/tests/polling_with_reqwest.rs"&gt;sandbox_http/polling_with_reqwest.rs at development · davidB/sandbox_http&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output of the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;running 1 test
110ns : check info, then continue, retry or follow
4.002665744s : check info, then continue, retry or follow
5.004217149s : check info, then continue, retry or follow
6.007647326s : check info, then continue, retry or follow
7.010080187s : check info, then continue, retry or follow
8.012471894s : check info, then continue, retry or follow
[tests/polling_with_reqwest.rs:152] &amp;amp;body = WorkOutput {
    nb_get_call: 4,
    duration: 8s,
}
test polling_with_reqwest ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.03s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🎉 Success:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first retry is after 4s because we defined on server as half of the duration of the work,&lt;/li&gt;
&lt;li&gt;Following call as a duration around 1s&lt;/li&gt;
&lt;li&gt;The http client doesn't included endpoint dedicated rules (no parse of the body, no build of url,...)&lt;/li&gt;
&lt;li&gt;Supporting an other endpoint with polling doesn't require additional code&lt;/li&gt;
&lt;li&gt;Bonus: support of downtime, cirsuit-break &amp;amp; rate-limit returning &lt;code&gt;Retry-After&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>http</category>
      <category>api</category>
      <category>rust</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Experimentations on Bazel: Python (3), linter &amp; pytest</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Mon, 03 May 2021 19:36:42 +0000</pubDate>
      <link>https://dev.to/davidb31/experimentations-on-bazel-python-3-linter-pytest-49oh</link>
      <guid>https://dev.to/davidb31/experimentations-on-bazel-python-3-linter-pytest-49oh</guid>
      <description>&lt;p&gt;In the previous articles,we saw how to launch linter for python with a custom rule, and in the one before how to launch pytest test. In this article, we'll see how to combine both usage in more friendly way (IMHO).&lt;/p&gt;

&lt;p&gt;Since the previous article, I did more experimentations (on my side), and I was limited by my current knowledge about how to implement rules (and managed dependency). But with bazel's &lt;a href="https://docs.bazel.build/versions/master/skylark/macros.html"&gt;macro&lt;/a&gt;, we could fix (or reduce) some issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplication of pytest's boilerplate every &lt;code&gt;BUILD.bazel&lt;/code&gt; and &lt;code&gt;*test.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;duplication between pytest and potential linter&lt;/li&gt;
&lt;li&gt;too many custom code to please bazel&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create the macro
&lt;/h2&gt;

&lt;p&gt;Create the empty &lt;code&gt;tools/pytest/BUILD.bazel&lt;/code&gt; , just to initiate the package.&lt;/p&gt;

&lt;p&gt;Create the macro &lt;code&gt;tools/pytest/defs.bzl&lt;/code&gt;, it is like the existing &lt;code&gt;py_test&lt;/code&gt; defined in &lt;code&gt;exp_python/webapp/BUILD.bazel&lt;/code&gt; but with the call inside a function and some part configurable.&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="s"&gt;"""experience wrap py_test"""&lt;/span&gt;

&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@rules_python//python:defs.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"py_test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@my_python_deps//:requirements.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"requirement"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pytest_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""
        Call pytest
    """&lt;/span&gt;
    &lt;span class="n"&gt;py_test&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;srcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s"&gt;"//tools/pytest:pytest_wrapper.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"//tools/pytest:pytest_wrapper.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s"&gt;"--capture=no"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"$(location :%s)"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;python_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PY3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;srcs_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PY3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pytest"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;load&lt;/code&gt; to import definition of symbols&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pytest_test&lt;/code&gt; is the macro, in fact a function that call py_test&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;//tools/pytest:pytest_wrapper.py&lt;/code&gt; is the wrapper to call pytest (see below)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;srcs&lt;/code&gt;, &lt;code&gt;deps&lt;/code&gt;, &lt;code&gt;args&lt;/code&gt; are the argument of that will customize&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;**kwargs&lt;/code&gt; allow to forward some args from the caller side for the part that are not overwitten by the macro&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;["$(location :%s)" % x for x in srcs]&lt;/code&gt; is to append at the end of the call the list of source file to process, to have the path of those file (srcs is a list of string at this point), we call &lt;code&gt;$(location ...)&lt;/code&gt; to have the path in the execution context of the py_test and and the prefix &lt;code&gt;:&lt;/code&gt; is to convert the file name into a label. It's hacky but it works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then create the wrapper that will call pytest &lt;code&gt;tools/pytest/pytest_wrapper.py&lt;/code&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="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pytest&lt;/span&gt;

&lt;span class="c1"&gt;# if using 'bazel test ...'
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]))&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Replace the current call to py_test by a call to hour macro in &lt;code&gt;exp_python/webapp/BUILD.bazel&lt;/code&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="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"//tools/pytest:defs.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pytest_test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# py_test(
#     name = "test",
#     srcs = [
#         "test.py",
#     ],
#     # main = "test.py",
#     args = [
#         "--capture=no",
#     ],
#     python_version = "PY3",
#     srcs_version = "PY3",
#     deps = [
#         ":webapp",
#         requirement("requests"),
#         requirement("fastapi"),
#         requirement("pytest"),
#     ],
# )
&lt;/span&gt;
&lt;span class="n"&gt;pytest_test&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="s"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;srcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"test.py"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s"&gt;":webapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"requests"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;# requirement("fastapi"),
&lt;/span&gt;    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;We keep commented the previous code for comparison, it's shorter and we move into a central shared place code that could be shared with other python package. Give a try...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel &lt;span class="nb"&gt;test&lt;/span&gt; //exp_python/webapp:test
ERROR: /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/BUILD.bazel:32:12: no such target &lt;span class="s1"&gt;'//tools/pytest:pytest_wrapper.py'&lt;/span&gt;: target &lt;span class="s1"&gt;'pytest_wrapper.py'&lt;/span&gt; not declared &lt;span class="k"&gt;in &lt;/span&gt;package &lt;span class="s1"&gt;'tools/pytest'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; however, a &lt;span class="nb"&gt;source &lt;/span&gt;file of this name exists.  &lt;span class="o"&gt;(&lt;/span&gt;Perhaps add &lt;span class="s1"&gt;'exports_files(["pytest_wrapper.py"])'&lt;/span&gt; to tools/pytest/BUILD?&lt;span class="o"&gt;)&lt;/span&gt; defined by /home/david/src/github.com/davidB/sandbox_bazel/tools/pytest/BUILD.bazel and referenced by &lt;span class="s1"&gt;'//exp_python/webapp:test'&lt;/span&gt;
ERROR: /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/BUILD.bazel:32:12: no such target &lt;span class="s1"&gt;'//tools/pytest:pytest_wrapper.py'&lt;/span&gt;: target &lt;span class="s1"&gt;'pytest_wrapper.py'&lt;/span&gt; not declared &lt;span class="k"&gt;in &lt;/span&gt;package &lt;span class="s1"&gt;'tools/pytest'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; however, a &lt;span class="nb"&gt;source &lt;/span&gt;file of this name exists.  &lt;span class="o"&gt;(&lt;/span&gt;Perhaps add &lt;span class="s1"&gt;'exports_files(["pytest_wrapper.py"])'&lt;/span&gt; to tools/pytest/BUILD?&lt;span class="o"&gt;)&lt;/span&gt; defined by /home/david/src/github.com/davidB/sandbox_bazel/tools/pytest/BUILD.bazel and referenced by &lt;span class="s1"&gt;'//exp_python/webapp:test'&lt;/span&gt;
ERROR: Analysis of target &lt;span class="s1"&gt;'//exp_python/webapp:test'&lt;/span&gt; failed&lt;span class="p"&gt;;&lt;/span&gt; build aborted: Analysis failed
INFO: Elapsed &lt;span class="nb"&gt;time&lt;/span&gt;: 0.704s
INFO: 0 processes.
FAILED: Build did NOT &lt;span class="nb"&gt;complete &lt;/span&gt;successfully &lt;span class="o"&gt;(&lt;/span&gt;18 packages loaded, 19 targets configured&lt;span class="o"&gt;)&lt;/span&gt;
FAILED: Build did NOT &lt;span class="nb"&gt;complete &lt;/span&gt;successfully &lt;span class="o"&gt;(&lt;/span&gt;18 packages loaded, 19 targets configured&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, we need to fix some stuff, because it's a macro we need to expose pytest_wrapper.py to caller of the macro. So just follow the recommendation and change the empty &lt;code&gt;tools/pytest/BUILD.bazel&lt;/code&gt; into&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="n"&gt;exports_files&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"pytest_wrapper.py"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retry...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel &lt;span class="nb"&gt;test&lt;/span&gt; //exp_python/webapp:test
INFO: Analyzed target //exp_python/webapp:test &lt;span class="o"&gt;(&lt;/span&gt;26 packages loaded, 15198 targets configured&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
INFO: Found 1 &lt;span class="nb"&gt;test &lt;/span&gt;target...
Target //exp_python/webapp:test up-to-date:
  bazel-bin/exp_python/webapp/test
INFO: Elapsed &lt;span class="nb"&gt;time&lt;/span&gt;: 2.242s, Critical Path: 0.99s
INFO: 5 processes: 3 internal, 2 linux-sandbox.
INFO: Build completed successfully, 5 total actions
//exp_python/webapp:test                                                 PASSED &lt;span class="k"&gt;in &lt;/span&gt;0.7s

Executed 1 out of 1 &lt;span class="nb"&gt;test&lt;/span&gt;: 1 &lt;span class="nb"&gt;test &lt;/span&gt;passes.
There were tests whose specified size is too big. Use the &lt;span class="nt"&gt;--test_verbose_timeout_warnings&lt;/span&gt; &lt;span class="nb"&gt;command &lt;/span&gt;line option to see which ones these aINFO: Build completed successfully, 5 total actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First victory.&lt;/p&gt;

&lt;p&gt;We can now remove the boilerblate to call pytest at the end of &lt;code&gt;exp_python/webapp/test.py&lt;/code&gt; because it is now provided by the shared &lt;code&gt;tools/pytest/pytest_wrapper.py&lt;/code&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="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;fastapi.testclient&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TestClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;exp_python.webapp.main&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TestClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_read_main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/status"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"UP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;You can mofidy the test to force the fail, and check if it'll fail as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Pytest to launch linter
&lt;/h2&gt;

&lt;p&gt;Pytest have lot of plugin/extension to launch tool like black, pylint, flake8, mypy,... So instead of create one linter call we can configure pytest run all linter.&lt;/p&gt;

&lt;p&gt;Add linter into &lt;code&gt;third_party/requirements.txt&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
#test
requests==2.25.1
pytest==6.1.2

#tools
black==20.8b1
pytest-black==0.3.12
pytest-mypy==0.8.1
pytest-pylint==0.18.0
pylint==2.8.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure &lt;code&gt;tools/pytest/defs.bzl&lt;/code&gt; to also launch linter and configure the call to pytest (it's like a script and a configuration)&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="s"&gt;"""experience wrap py_test"""&lt;/span&gt;

&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@rules_python//python:defs.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"py_test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@my_python_deps//:requirements.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"requirement"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pytest_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""
        Call pytest
    """&lt;/span&gt;
    &lt;span class="n"&gt;py_test&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;srcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s"&gt;"//tools/pytest:pytest_wrapper.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"//tools/pytest:pytest_wrapper.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s"&gt;"--capture=no"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"--black"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"--pylint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"--mypy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"$(location :%s)"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;python_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PY3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;srcs_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PY3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pytest"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pytest-black"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pytest-pylint"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pytest-mypy"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Run it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel &lt;span class="nb"&gt;test&lt;/span&gt; //exp_python/webapp:test
INFO: Analyzed target //exp_python/webapp:test &lt;span class="o"&gt;(&lt;/span&gt;20 packages loaded, 2332 targets configured&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
INFO: Found 1 &lt;span class="nb"&gt;test &lt;/span&gt;target...
FAIL: //exp_python/webapp:test &lt;span class="o"&gt;(&lt;/span&gt;see /home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/execroot/__main__/bazel-out/k8-fastbuild/testlogs/exp_python/webapp/test/test.log&lt;span class="o"&gt;)&lt;/span&gt;
INFO: From Testing //exp_python/webapp:test:
&lt;span class="o"&gt;====================&lt;/span&gt; Test output &lt;span class="k"&gt;for&lt;/span&gt; //exp_python/webapp:test:
&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 linux &lt;span class="nt"&gt;--&lt;/span&gt; Python 3.9.2, pytest-6.1.2, py-1.10.0, pluggy-0.13.1
rootdir: /home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/sandbox/linux-sandbox/3/execroot/__main__/bazel-out/k8-fastbuild/bin/exp_python/webapp/test.runfiles/__main__
plugins: black-0.3.12, pylint-0.18.0, mypy-0.8.1
collected 5 items
&lt;span class="nt"&gt;--------------------------------------------------------------------------------&lt;/span&gt;
Linting files
....Unable to create directory /home/david/.pylint.d
Unable to create file /home/david/.pylint.d/exp_python.__init__1.stats: &lt;span class="o"&gt;[&lt;/span&gt;Errno 2] No such file or directory: &lt;span class="s1"&gt;'/home/david/.pylint.d/exp_python.__init__1.stats'&lt;/span&gt;

&lt;span class="nt"&gt;--------------------------------------------------------------------------------&lt;/span&gt;

exp_python/webapp/test.py FFF..

&lt;span class="o"&gt;===================================&lt;/span&gt; FAILURES &lt;span class="o"&gt;===================================&lt;/span&gt;
______________________ &lt;span class="o"&gt;[&lt;/span&gt;pylint] exp_python/webapp/test.py ______________________
C:  1, 0: Missing module docstring &lt;span class="o"&gt;(&lt;/span&gt;missing-module-docstring&lt;span class="o"&gt;)&lt;/span&gt;
C:  7, 0: Missing &lt;span class="k"&gt;function &lt;/span&gt;or method docstring &lt;span class="o"&gt;(&lt;/span&gt;missing-function-docstring&lt;span class="o"&gt;)&lt;/span&gt;
__________________________ exp_python/webapp/test.py ___________________________
1: error: Cannot find implementation or library stub &lt;span class="k"&gt;for &lt;/span&gt;module named &lt;span class="s1"&gt;'fastapi.testclient'&lt;/span&gt;
1: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
_________________________________ &lt;span class="nb"&gt;test &lt;/span&gt;session _________________________________
mypy exited with status 1.
&lt;span class="o"&gt;=====================================&lt;/span&gt; mypy &lt;span class="o"&gt;=====================================&lt;/span&gt;
exp_python/webapp/main.py:1: error: Cannot find implementation or library stub &lt;span class="k"&gt;for &lt;/span&gt;module named &lt;span class="s1"&gt;'fastapi'&lt;/span&gt;
Found 2 errors &lt;span class="k"&gt;in &lt;/span&gt;2 files &lt;span class="o"&gt;(&lt;/span&gt;checked 1 &lt;span class="nb"&gt;source &lt;/span&gt;file&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;===========================&lt;/span&gt; short &lt;span class="nb"&gt;test &lt;/span&gt;summary info &lt;span class="o"&gt;============================&lt;/span&gt;
FAILED exp_python/webapp/test.py::PYLINT
FAILED exp_python/webapp/test.py::mypy
FAILED exp_python/webapp/test.py::mypy-status
&lt;span class="o"&gt;=========================&lt;/span&gt; 3 failed, 2 passed &lt;span class="k"&gt;in &lt;/span&gt;1.99s &lt;span class="o"&gt;==========================&lt;/span&gt;
&lt;span class="o"&gt;================================================================================&lt;/span&gt;
Target //exp_python/webapp:test up-to-date:
  bazel-bin/exp_python/webapp/test
INFO: Elapsed &lt;span class="nb"&gt;time&lt;/span&gt;: 3.841s, Critical Path: 3.45s
INFO: 5 processes: 3 internal, 2 linux-sandbox.
INFO: Build completed, 1 &lt;span class="nb"&gt;test &lt;/span&gt;FAILED, 5 total actions
//exp_python/webapp:test                                                 FAILED &lt;span class="k"&gt;in &lt;/span&gt;3.0s
  /home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/execroot/__main__/bazel-out/k8-fastbuild/testlogs/exp_python/webapp/test/test.log

INFO: Build completed, 1 &lt;span class="nb"&gt;test &lt;/span&gt;FAILED, 5 total actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seems to work (or failed as expected, linter detect issue)&lt;/p&gt;

&lt;p&gt;Now extend &lt;code&gt;pytest_test&lt;/code&gt; to lint over every *.py of the package (and let pytest detects test). As we check every python file, we also need to add all their dependencies.&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="n"&gt;pytest_test&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="s"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;srcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"*.py"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s"&gt;":run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;":webapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"requests"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fastapi"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;To fix the linter issue (I comments mypy, not useful for the purpose of this article), provide a configuration file for pylint and disable a pylint rule localy into &lt;code&gt;run.py&lt;/code&gt; (full code available into the repo, see bellow). So the final &lt;code&gt;tools/pytest/defs.bzl&lt;/code&gt; looks like (with a configuration file for the demo, provide as data to &lt;code&gt;py_test&lt;/code&gt; and as args)&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="s"&gt;"""Wrap pytest"""&lt;/span&gt;

&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@rules_python//python:defs.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"py_test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@my_python_deps//:requirements.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"requirement"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pytest_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""
        Call pytest
    """&lt;/span&gt;
    &lt;span class="n"&gt;py_test&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;srcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s"&gt;"//tools/pytest:pytest_wrapper.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"//tools/pytest:pytest_wrapper.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s"&gt;"--capture=no"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"--black"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"--pylint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"--pylint-rcfile=$(location //tools/pytest:.pylintrc)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;# "--mypy",
&lt;/span&gt;        &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"$(location :%s)"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;python_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PY3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;srcs_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PY3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pytest"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pytest-black"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pytest-pylint"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="c1"&gt;# requirement("pytest-mypy"),
&lt;/span&gt;        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s"&gt;"//tools/pytest:.pylintrc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;And modify &lt;code&gt;tools/pytest/BUILD.bazel&lt;/code&gt; to export configuration files.&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="n"&gt;exports_files&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s"&gt;"pytest_wrapper.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;".pylintrc"&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;h2&gt;
  
  
  Clean up
&lt;/h2&gt;

&lt;p&gt;Remove code and reference to pytest_check (that we introduce previously), it's no longer needed. Now we have&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ tree -a -I '.vscode|bazel-*|.venv|.git'                                                                                      21:16:36
.
├── .bazelrc
├── .bazelversion
├── BUILD.bazel
├── exp_genrule
│   ├── a.txt
│   ├── b.txt
│   └── BUILD.bazel
├── exp_python
│   └── webapp
│       ├── BUILD.bazel
│       ├── main.py
│       ├── __pycache__
│       │   └── main.cpython-39.pyc
│       ├── run.py
│       └── test.py
├── .github
│   ├── dependabot.yml
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .python-version
├── setup_localdev.sh
├── third_party
│   ├── BUILD.bazel
│   └── requirements.txt
├── tools
│   ├── pytest
│   │   ├── BUILD.bazel
│   │   ├── defs.bzl
│   │   ├── .pylintrc
│   │   └── pytest_wrapper.py
│   └── workspace_status.sh
└── WORKSPACE.bazel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Limits of the solution
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Dependencies of test, runtime, tools are all mixed (no isolation), but it's the common (crappy ?) way to work in python ecosystem.&lt;/li&gt;
&lt;li&gt;The macro is harder to extract as a workspace, tools for reuse into an other project, but in the other side the code is small enough to be copied and customized for each need&lt;/li&gt;
&lt;li&gt;No split in the output of bazel every linter and test will be show as 1 test into report of bazel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end, currently it's the most pleasant solution I found to have pytest, linters run by bazel without too many (and duplicate) boilerplate into each package. A central configuration place, and pretty simple to extends if pytest as the extension (else we can tweak the &lt;code&gt;pytest_wrapper.py&lt;/code&gt; to do complementary stuff by example).&lt;/p&gt;

&lt;h2&gt;
  
  
  To be continued
&lt;/h2&gt;

&lt;p&gt;It's not the end, we'll continue to setup other stuff like depending of another package (a python lib), make a docker image,...&lt;/p&gt;

&lt;p&gt;The sandbox_bazel is hosted on github (not with the same history, due to errors), use tag to have the expected view at end of article: &lt;a href="https://github.com/davidB/sandbox_bazel/tree/article/6_python_3"&gt;article/6_python_3&lt;/a&gt;. I'll be happy to have your comments on this article, or to discuss on github repo.&lt;/p&gt;

</description>
      <category>bazel</category>
      <category>python</category>
      <category>pytest</category>
      <category>pylint</category>
    </item>
    <item>
      <title>Experimentations on Bazel: Python (2), linter</title>
      <dc:creator>David Bernard</dc:creator>
      <pubDate>Sun, 02 May 2021 15:34:12 +0000</pubDate>
      <link>https://dev.to/davidb31/experimentations-on-bazel-python-2-linter-52</link>
      <guid>https://dev.to/davidb31/experimentations-on-bazel-python-2-linter-52</guid>
      <description>&lt;p&gt;Time to add code formatter, checkers, linters and other tools that help to keep python code runnable, less buggy and more resilient to evolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup formatter for Editor
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/psf/black"&gt;psf/black: The uncompromising Python code formatter&lt;/a&gt; will be used. Add it to &lt;code&gt;thrid_party/requirements.txt&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...

#tools
black==20.8b1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relaunch &lt;code&gt;./setup_localdev.sh&lt;/code&gt; and configure your editor to use it (if its can use virtual env), for example for vscode &lt;code&gt;.vscode/settings.json&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"python.pythonPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".venv/bin/python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"python.formatting.provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"black"&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;h2&gt;
  
  
  Setup formatter for Bazel with bazel-linting-system
&lt;/h2&gt;

&lt;p&gt;But bazel currently doesn't use it (to format or to check the format). After a quick search, &lt;a href="https://github.com/thundergolfer/bazel-linting-system"&gt;thundergolfer/bazel-linting-system: 🌿💚 Experimental system for registering, configuring, and invoking source-code linters in Bazel.&lt;/a&gt; seems to be the solution, it's based on bazel's aspect something that we didn't explore or experiment yet. So give it a try by following instruction&lt;/p&gt;

&lt;p&gt;Add to &lt;code&gt;WORKSPACE.bazel&lt;/code&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="n"&gt;http_archive&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="s"&gt;"linting_system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;strip_prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"bazel-linting-system-0.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/thundergolfer/bazel-linting-system/archive/v0.4.0.zip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@linting_system//repositories:repositories.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;linting_sys_repositories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"repositories"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;linting_sys_repositories&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@linting_system//repositories:go_repositories.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;linting_sys_deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"go_deps"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;linting_sys_deps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;tools/linting/aspect.bzl&lt;/code&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="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@linting_system//:generator.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"linting_aspect_generator"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;lint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;linting_aspect_generator&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="s"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;linters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s"&gt;"@//tools/linting:python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;tools/linting/BUILD.bazel&lt;/code&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="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@linting_system//:rules.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"linter"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_visibility&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'//visibility:public'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;linter&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="s"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;executable_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/usr/local/bin/black"&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;Run it to test&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bazel build //... &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--aspects&lt;/span&gt; //tools/linting:aspect.bzl%lint &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output_groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;report

...
bazel-out/k8-fastbuild/bin/exp_python/double/test_linter_exe: line 26: /usr/local/bin/black: No such file or directory
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As expected 😉 it failed because we do not have &lt;code&gt;black&lt;/code&gt; installed on the system, and it's better when the build system take care of the installation of tool (for repeatability, and to ease the developer setup (less step and &lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;tools/linting/python_linter.py&lt;/code&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="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;black&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;black&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;manual operation)). But we can try to create a tool that will call black (or any other linter).&lt;/p&gt;

&lt;p&gt;Modify &lt;code&gt;tools/linting/BUILD.bazel&lt;/code&gt; to build &lt;code&gt;python_linter&lt;/code&gt; and to use it as linter.&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="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@linting_system//:rules.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"linter"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@rules_python//python:defs.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"py_binary"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@my_python_deps//:requirements.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"requirement"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_visibility&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"//visibility:public"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;py_binary&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="s"&gt;"python_linter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;srcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"python_linter.py"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;python_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PY3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;srcs_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PY3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"black"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Try to run it python_linter&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel run //tools/linting:python_linter &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt;        
...
python_linter.py, version 20.8b1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OK, seems to work, update linter to use &lt;code&gt;python_linter&lt;/code&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="p"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;linter&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="s"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;executable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"python_linter"&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;Note that we use &lt;code&gt;executable&lt;/code&gt; and not &lt;code&gt;executable_path&lt;/code&gt;, after look at the rule description&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="n"&gt;linter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_linter_impl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"executable_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Absolute path to the linter that will run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mandatory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"executable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Label for an executable linter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;allow_files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Configuration file for linter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"config_option"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"The option used by the linter to pass a path to a configuration file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"config_str"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Raw string configuration options to be passed to linter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try again&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel build //... &lt;span class="se"&gt;\ &lt;/span&gt;                                                                                                         20:48:18
          &lt;span class="nt"&gt;--aspects&lt;/span&gt; //tools/linting:aspect.bzl%lint &lt;span class="se"&gt;\&lt;/span&gt;
          &lt;span class="nt"&gt;--output_groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;report
DEBUG: Rule &lt;span class="s1"&gt;'linting_system'&lt;/span&gt; indicated that a canonical reproducible form can be obtained by modifying arguments sha256 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"a254c73bdde03214b62cacdb570229ed1a1814a2ed749448a1db4e90b18ac0a1"&lt;/span&gt;
DEBUG: Repository linting_system instantiated at:
  /home/david/src/github.com/davidB/sandbox_bazel/WORKSPACE.bazel:41:13: &lt;span class="k"&gt;in&lt;/span&gt; &amp;lt;toplevel&amp;gt;
Repository rule http_archive defined at:
  /home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/external/bazel_tools/tools/build_defs/repo/http.bzl:336:31: &lt;span class="k"&gt;in&lt;/span&gt; &amp;lt;toplevel&amp;gt;
INFO: Analyzed 9 targets &lt;span class="o"&gt;(&lt;/span&gt;0 packages loaded, 0 targets configured&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
INFO: Found 9 targets...
ERROR: /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/BUILD.bazel:32:10: MirrorAndLint exp_python/webapp/__linting_system/run/exp_python/webapp/run.py failed: &lt;span class="o"&gt;(&lt;/span&gt;Exit 1&lt;span class="o"&gt;)&lt;/span&gt;: run_linter_exe failed: error executing &lt;span class="nb"&gt;command &lt;/span&gt;bazel-out/k8-fastbuild/bin/exp_python/webapp/run_linter_exe &lt;span class="s1"&gt;'exp_python/webapp/run.py;bazel-out/k8-fastbuild/bin/exp_python/webapp/__linting_system/run/exp_python/webapp/run.py'&lt;/span&gt;

Use &lt;span class="nt"&gt;--sandbox_debug&lt;/span&gt; to see verbose messages from the sandbox run_linter_exe failed: error executing &lt;span class="nb"&gt;command &lt;/span&gt;bazel-out/k8-fastbuild/bin/exp_python/webapp/run_linter_exe &lt;span class="s1"&gt;'exp_python/webapp/run.py;bazel-out/k8-fastbuild/bin/exp_python/webapp/__linting_system/run/exp_python/webapp/run.py'&lt;/span&gt;

Use &lt;span class="nt"&gt;--sandbox_debug&lt;/span&gt; to see verbose messages from the sandbox
Traceback &lt;span class="o"&gt;(&lt;/span&gt;most recent call last&lt;span class="o"&gt;)&lt;/span&gt;:
  File &lt;span class="s2"&gt;"/home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/sandbox/linux-sandbox/38/execroot/__main__/bazel-out/host/bin/tools/linting/python_linter"&lt;/span&gt;, line 388, &lt;span class="k"&gt;in&lt;/span&gt; &amp;lt;module&amp;gt;
    Main&lt;span class="o"&gt;()&lt;/span&gt;
  File &lt;span class="s2"&gt;"/home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/sandbox/linux-sandbox/38/execroot/__main__/bazel-out/host/bin/tools/linting/python_linter"&lt;/span&gt;, line 288, &lt;span class="k"&gt;in &lt;/span&gt;Main
    module_space &lt;span class="o"&gt;=&lt;/span&gt; FindModuleSpace&lt;span class="o"&gt;()&lt;/span&gt;
  File &lt;span class="s2"&gt;"/home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/sandbox/linux-sandbox/38/execroot/__main__/bazel-out/host/bin/tools/linting/python_linter"&lt;/span&gt;, line 118, &lt;span class="k"&gt;in &lt;/span&gt;FindModuleSpace
    raise AssertionError&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Cannot find .runfiles directory for %s'&lt;/span&gt; % sys.argv[0]&lt;span class="o"&gt;)&lt;/span&gt;
AssertionError: Cannot find .runfiles directory &lt;span class="k"&gt;for &lt;/span&gt;bazel-out/host/bin/tools/linting/python_linter
INFO: Elapsed &lt;span class="nb"&gt;time&lt;/span&gt;: 0.151s, Critical Path: 0.04s
INFO: 5 processes: 5 internal.
FAILED: Build did NOT &lt;span class="nb"&gt;complete &lt;/span&gt;successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What does it mean ? I don't know and I don't know how to fix it. I tried without success&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;executable = "//tools/linting:python_linter",&lt;/code&gt; same error&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;executable_path = "$(location //tools/linting:python_linter)",&lt;/code&gt; failed with &lt;code&gt;location: command not found&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep the experimentation on a branch &lt;a href="https://github.com/davidB/sandbox_bazel/tree/exp/linting_system"&gt;exp/linting_system&lt;/a&gt; and we'll try something different, make out own tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make a build tool as part of project
&lt;/h2&gt;

&lt;p&gt;It's something interesting to try, create as part of the project a tool to build the project. Start by revert changes made in the previous section, but keep it as inspiration to start.&lt;br&gt;
Create &lt;code&gt;tools/python_check/BUILD.bazel&lt;/code&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="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@rules_python//python:defs.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"py_binary"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@my_python_deps//:requirements.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"requirement"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_visibility&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"//visibility:public"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;py_binary&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="s"&gt;"python_check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;srcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"python_check.py"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;python_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PY3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;srcs_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PY3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"black"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;tools/python_check/python_check.py&lt;/code&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="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;black&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;black&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel run //tools/python_check &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt; 
python_check.py, version 20.8b1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now try to use it in &lt;code&gt;exp_python/webapp/BUILD.bazel&lt;/code&gt; by adding&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="n"&gt;genrule&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="s"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;srcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"**/*.py"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="n"&gt;outs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"check.log"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;cmd_bash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""(
        $(location //tools/python_check) --check --verbose $(SRCS)
        ) | tee $@
    """&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"//tools/python_check"&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;Try&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ bazel build //exp_python/webapp:check 
...
/home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/main.py already well formatted, good job.
would reformat /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/run.py
would reformat /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/test.py
Oh no! 💥 💔 💥
2 files would be reformatted, 1 file would be left unchanged.
... 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Basic seems to work. What happens if instead of "--check", we let black format the code ?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/main.py already well formatted, good job.
error: cannot format /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/run.py: &lt;span class="o"&gt;[&lt;/span&gt;Errno 30] Read-only file system: &lt;span class="s1"&gt;'/home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/run.py'&lt;/span&gt;
error: cannot format /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/test.py: &lt;span class="o"&gt;[&lt;/span&gt;Errno 30] Read-only file system: &lt;span class="s1"&gt;'/home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/test.py'&lt;/span&gt;
Oh no! 💥 💔 💥
1 file left unchanged, 2 files failed to reformat.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bazel block us to change source file, it's a pain for a formatter, but it's not a bad idea and it follows the way that bazel build commands could be run on remote build server (via CI or bazel remote execution), and that source is own and managed by the developer. But maybe bazel run could ?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ bazel run //tools/python_check -- --check $PWD/exp_python/**/*.py
...
would reformat /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/run.py
would reformat /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/test.py
Oh no! 💥 💔 💥
2 files would be reformatted, 1 file would be left unchanged.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$PWD&lt;/code&gt; because when running, the current folder is modified, so without absolute path we got &lt;code&gt;Error: Invalid value for '[SRC]...': Path 'exp_python/webapp/main.py' does not exist.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;**/*.py&lt;/code&gt; should be change if your shell doesn't support this syntax.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try to format&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ bazel run //tools/python_check -- $PWD/exp_python/**/*.py
...
INFO: Build completed successfully, 1 total action
reformatted /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/run.py
reformatted /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/test.py
All done! ✨ 🍰 ✨
2 files reformatted, 1 file left unchanged.

sandbox_bazel on  development [!?⇡] via 🐍 v3.9.4 
❯ bazel run //tools/python_check -- --check $PWD/exp_python/**/*.py
...
INFO: Build completed successfully, 1 total action
All done! ✨ 🍰 ✨
3 files would be left unchanged.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hourra, we have a &lt;code&gt;bazel build&lt;/code&gt; command able to check the code and some &lt;code&gt;bazel run&lt;/code&gt; able to check or to update the source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linting &amp;amp; check as test
&lt;/h2&gt;

&lt;p&gt;The goal all of checkers and linters is to evaluate the quality of the code and to detect issue and to suggest improvement, like for test. So it'll make more sens to run checks via &lt;code&gt;bazel test&lt;/code&gt; than &lt;code&gt;bazel build&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel &lt;span class="nb"&gt;test&lt;/span&gt; //exp_python/...
INFO: Analyzed 4 targets &lt;span class="o"&gt;(&lt;/span&gt;0 packages loaded, 0 targets configured&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
INFO: Found 3 targets and 1 &lt;span class="nb"&gt;test &lt;/span&gt;target...
INFO: Elapsed &lt;span class="nb"&gt;time&lt;/span&gt;: 0.091s, Critical Path: 0.01s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
//exp_python/webapp:test                                        &lt;span class="o"&gt;(&lt;/span&gt;cached&lt;span class="o"&gt;)&lt;/span&gt; PASSED &lt;span class="k"&gt;in &lt;/span&gt;0.7s

Executed 0 out of 1 &lt;span class="nb"&gt;test&lt;/span&gt;: 1 &lt;span class="nb"&gt;test &lt;/span&gt;passes.
There were tests whose specified size is too big. Use the &lt;span class="nt"&gt;--test_verbose_timeout_warnings&lt;/span&gt; &lt;span class="nb"&gt;command &lt;/span&gt;line option to see which ones these aINFO: Build completed successfully, 1 total action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can add a &lt;a href="https://docs.bazel.build/versions/master/be/general.html#test_suite"&gt;test_suite&lt;/a&gt; into &lt;code&gt;exp_python/webapp/BUILD.bazel&lt;/code&gt; that depends of both &lt;code&gt;py_test(name="test"...)&lt;/code&gt; and our &lt;code&gt;genrule(name="check"...)&lt;/code&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="n"&gt;test_suite&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="s"&gt;"quality"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel &lt;span class="nb"&gt;test&lt;/span&gt; //exp_python/...                                                                                                  12:07:31
ERROR: /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/BUILD.bazel:55:11: &lt;span class="k"&gt;in &lt;/span&gt;test_suite rule &lt;span class="s1"&gt;'//exp_python/webapp:quality'&lt;/span&gt;: expecting a &lt;span class="nb"&gt;test &lt;/span&gt;or a test_suite rule but &lt;span class="s1"&gt;'//exp_python/webapp:check'&lt;/span&gt; is not one.
WARNING: Target pattern parsing failed.
INFO: Analyzed 4 targets &lt;span class="o"&gt;(&lt;/span&gt;1 packages loaded, 7 targets configured&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
INFO: Found 3 targets and 1 &lt;span class="nb"&gt;test &lt;/span&gt;target...
INFO: From Executing genrule //exp_python/webapp:check:
/home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/main.py wasn&lt;span class="s1"&gt;'t modified on disk since last run.
/home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/run.py wasn'&lt;/span&gt;t modified on disk since last run.
/home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/test.py wasn&lt;span class="s1"&gt;'t modified on disk since last run.
All done! ✨ 🍰 ✨
3 files would be left unchanged.
ERROR: command succeeded, but there were errors parsing the target pattern
INFO: Elapsed time: 0.408s, Critical Path: 0.27s
INFO: 2 processes: 1 internal, 1 linux-sandbox.
FAILED: Build did NOT complete successfully
//exp_python/webapp:test                                        (cached) PASSED in 0.7s

Executed 0 out of 1 test: 1 test passes.
There were tests whose specified size is too big. Use the --test_verbose_timeout_warnings command line option to see which ones these are.
All tests passed but there were other errors during the build.
FAILED: Build did NOT complete successfully
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check&lt;/code&gt; is executed but bazel is not happy in accordance to the doc of &lt;a href="https://docs.bazel.build/versions/master/be/general.html#test_suite.tests"&gt;test_suite.tests&lt;/a&gt; (&lt;a href="https://docs.bazel.build/versions/master/be/general.html#genrule"&gt;genrule&lt;/a&gt; are like &lt;code&gt;*_binary&lt;/code&gt;)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No *_binary targets are accepted however, even if they happen to run a test.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's time create our first custom rule (inspired by the &lt;a href="https://docs.bazel.build/versions/master/skylark/rules-tutorial.html"&gt;Rules Tutorial - Bazel&lt;/a&gt; and the &lt;a href="https://github.com/bazelbuild/examples/tree/master/rules"&gt;examples/rules at master · bazelbuild/examples&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;tools/python_check/defs.bzl&lt;/code&gt; where we create a rule &lt;code&gt;python_check&lt;/code&gt; that try to mimic the &lt;a href="https://docs.bazel.build/versions/master/be/general.html#genrule"&gt;genrule&lt;/a&gt; that we want to convert&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="s"&gt;"""Launch the python_check tool"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_python_check_test_impl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"--check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--verbose"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_shell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;progress_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Check into %s"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;short_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"(%s %s) | tee '%s'"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;python_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_python_check_test_impl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"srcs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allow_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"log"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mandatory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;executable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"exec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;allow_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"//tools/python_check"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;A rule is composed of the declaration &lt;code&gt;xxx = rule(...)&lt;/code&gt; and its implementation.&lt;/p&gt;

&lt;p&gt;Then replace the &lt;code&gt;genrule(name="check"...)&lt;/code&gt; by a call to &lt;code&gt;python_check&lt;/code&gt; into &lt;code&gt;exp_python/webapp/BUILD.bazel&lt;/code&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="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@rules_python//python:defs.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"py_binary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"py_library"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"py_test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@my_python_deps//:requirements.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"requirement"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"//tools/python_check:defs.bzl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"python_check"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;span class="n"&gt;python_check&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="s"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;srcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"**/*.py"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"check.log"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel build //exp_python/webapp:check                                                                                        13:31:43
INFO: Analyzed target //exp_python/webapp:check &lt;span class="o"&gt;(&lt;/span&gt;1 packages loaded, 4 targets configured&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
INFO: Found 1 target...
Target //exp_python/webapp:check up-to-date:
  bazel-bin/exp_python/webapp/check.log
INFO: Elapsed &lt;span class="nb"&gt;time&lt;/span&gt;: 0.092s, Critical Path: 0.01s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;now make python_check a test rule:&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="n"&gt;python_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&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;Re-try&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel &lt;span class="nb"&gt;test&lt;/span&gt; //exp_python/...
ERROR: /home/david/src/github.com/davidB/sandbox_bazel/tools/python_check/defs.bzl:14:20: Invalid rule class name &lt;span class="s1"&gt;'python_check'&lt;/span&gt;, &lt;span class="nb"&gt;test &lt;/span&gt;rule class names must end with &lt;span class="s1"&gt;'_test'&lt;/span&gt; and other rule classes must not
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OK so we need to rename &lt;code&gt;python_check&lt;/code&gt; into &lt;code&gt;python_check_test&lt;/code&gt; into &lt;code&gt;tools/python_check/defs.bzl&lt;/code&gt; and &lt;code&gt;exp_python/webapp/BUILD.bazel&lt;/code&gt; and re-try&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel &lt;span class="nb"&gt;test&lt;/span&gt; //exp_python/...
ERROR: /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/BUILD.bazel:45:18: &lt;span class="k"&gt;in &lt;/span&gt;python_check_test rule //exp_python/webapp:check: 
/home/david/src/github.com/davidB/sandbox_bazel/tools/python_check/defs.bzl:3:5: The rule &lt;span class="s1"&gt;'python_check_test'&lt;/span&gt; is executable. It needs to create an executable File and pass it as the &lt;span class="s1"&gt;'executable'&lt;/span&gt; parameter to the DefaultInfo it returns.
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seems that from the error message and the sample &lt;a href="https://github.com/bazelbuild/examples/blob/master/rules/test_rule/line_length.bzl"&gt;examples/line_length.bzl at master · bazelbuild/examples&lt;/a&gt;, that we should modify also our &lt;code&gt;_impl&lt;/code&gt; function to return a DefaultInfo  instead of running the command directly. As a test we can also remove the &lt;code&gt;log&lt;/code&gt; attributes because the output of test is the exit code and stdout/stderr managed by bazel directly. So Update &lt;code&gt;tools/python_check/defs.bzl&lt;/code&gt; to&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="s"&gt;"""Launch the python_check tool"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_python_check_test_impl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"--check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--verbose"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"%s %s"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Write the file, it is executed by 'bazel test'.
&lt;/span&gt;    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# To ensure the files needed by the command are available, we put them in
&lt;/span&gt;    &lt;span class="c1"&gt;# the runfiles.
&lt;/span&gt;    &lt;span class="n"&gt;runfiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runfiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DefaultInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runfiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runfiles&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="n"&gt;python_check_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_python_check_test_impl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"srcs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allow_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;executable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"exec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;allow_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"//tools/python_check"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&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;Remove the &lt;code&gt;log&lt;/code&gt; attribute on the caller side&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="n"&gt;python_check_test&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="s"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;srcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"**/*.py"&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;Re-try&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel &lt;span class="nb"&gt;test&lt;/span&gt; //exp_python/...                                                                                                  14:54:01
INFO: Analyzed 4 targets &lt;span class="o"&gt;(&lt;/span&gt;1 packages loaded, 7 targets configured&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
INFO: Found 2 targets and 2 &lt;span class="nb"&gt;test &lt;/span&gt;targets...
FAIL: //exp_python/webapp:check &lt;span class="o"&gt;(&lt;/span&gt;see /home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/execroot/__main__/bazel-out/k8-fastbuild/testlogs/exp_python/webapp/check/test.log&lt;span class="o"&gt;)&lt;/span&gt;
INFO: From Testing //exp_python/webapp:check:
&lt;span class="o"&gt;====================&lt;/span&gt; Test output &lt;span class="k"&gt;for&lt;/span&gt; //exp_python/webapp:check:
/home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/sandbox/linux-sandbox/15/execroot/__main__/bazel-out/k8-fastbuild/bin/exp_python/webapp/check.runfiles/__main__/exp_python/webapp/check: line 1: bazel-out/k8-opt-exec-2B5CBBC6/bin/tools/python_check/python_check: No such file or directory
&lt;span class="o"&gt;================================================================================&lt;/span&gt;
INFO: Elapsed &lt;span class="nb"&gt;time&lt;/span&gt;: 0.210s, Critical Path: 0.06s
INFO: 2 processes: 2 linux-sandbox.
INFO: Build completed, 1 &lt;span class="nb"&gt;test &lt;/span&gt;FAILED, 2 total actions
//exp_python/webapp:test                                        &lt;span class="o"&gt;(&lt;/span&gt;cached&lt;span class="o"&gt;)&lt;/span&gt; PASSED &lt;span class="k"&gt;in &lt;/span&gt;0.7s
//exp_python/webapp:check                                                FAILED &lt;span class="k"&gt;in &lt;/span&gt;0.0s
  /home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/execroot/__main__/bazel-out/k8-fastbuild/testlogs/exp_python/webapp/check/test.log

Executed 1 out of 2 tests: 1 &lt;span class="nb"&gt;test &lt;/span&gt;passes and 1 fails locally.
There were tests whose specified size is too big. Use the &lt;span class="nt"&gt;--test_verbose_timeout_warnings&lt;/span&gt; &lt;span class="nb"&gt;command &lt;/span&gt;line option to see which ones these a
INFO: Build completed, 1 &lt;span class="nb"&gt;test &lt;/span&gt;FAILED, 2 total actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failed but looks like what we expect &lt;code&gt;check&lt;/code&gt; is now part of the test flow. But we still have issues with the path of our python_check executable or/and the way we defined the &lt;a href="https://docs.bazel.build/versions/master/skylark/rules.html#runfiles"&gt;runfiles&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;try to replace &lt;code&gt;ctx.executable.tool.path&lt;/code&gt; by &lt;code&gt;ctx.executable.tool.short_path&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel &lt;span class="nb"&gt;test&lt;/span&gt; //exp_python/...                                                                                                  16:29:55
INFO: Analyzed 4 targets &lt;span class="o"&gt;(&lt;/span&gt;1 packages loaded, 7 targets configured&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
INFO: Found 2 targets and 2 &lt;span class="nb"&gt;test &lt;/span&gt;targets...
FAIL: //exp_python/webapp:check &lt;span class="o"&gt;(&lt;/span&gt;see /home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/execroot/__main__/bazel-out/k8-fastbuild/testlogs/exp_python/webapp/check/test.log&lt;span class="o"&gt;)&lt;/span&gt;
INFO: From Testing //exp_python/webapp:check:
&lt;span class="o"&gt;====================&lt;/span&gt; Test output &lt;span class="k"&gt;for&lt;/span&gt; //exp_python/webapp:check:
Traceback &lt;span class="o"&gt;(&lt;/span&gt;most recent call last&lt;span class="o"&gt;)&lt;/span&gt;:
  File &lt;span class="s2"&gt;"/home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/sandbox/linux-sandbox/44/execroot/__main__/bazel-out/k8-fastbuild/bin/exp_python/webapp/check.runfiles/__main__/tools/python_check/python_check"&lt;/span&gt;, line 388, &lt;span class="k"&gt;in&lt;/span&gt; &amp;lt;module&amp;gt;
    Main&lt;span class="o"&gt;()&lt;/span&gt;
  File &lt;span class="s2"&gt;"/home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/sandbox/linux-sandbox/44/execroot/__main__/bazel-out/k8-fastbuild/bin/exp_python/webapp/check.runfiles/__main__/tools/python_check/python_check"&lt;/span&gt;, line 322, &lt;span class="k"&gt;in &lt;/span&gt;Main
    assert os.path.exists&lt;span class="o"&gt;(&lt;/span&gt;main_filename&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="se"&gt;\&lt;/span&gt;
AssertionError: Cannot &lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="s1"&gt;'/home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/sandbox/linux-sandbox/44/execroot/__main__/bazel-out/k8-fastbuild/bin/exp_python/webapp/check.runfiles/__main__/tools/python_check/python_check.py'&lt;/span&gt;: file not found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It means that &lt;code&gt;short_path&lt;/code&gt; works but the tool is missing some dependencies. In fact we only include the entry file and not its runtime dependencies. So change the way to build runfiles by&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="n"&gt;runfiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runfiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runfiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runfiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DefaultInfo&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;default_runfiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Then&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel &lt;span class="nb"&gt;test&lt;/span&gt; //exp_python/...
INFO: Analyzed 4 targets &lt;span class="o"&gt;(&lt;/span&gt;0 packages loaded, 0 targets configured&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
INFO: Found 2 targets and 2 &lt;span class="nb"&gt;test &lt;/span&gt;targets...
INFO: Elapsed &lt;span class="nb"&gt;time&lt;/span&gt;: 0.895s, Critical Path: 0.80s
INFO: 3 processes: 4 linux-sandbox.
INFO: Build completed successfully, 3 total actions
//exp_python/webapp:check                                                PASSED &lt;span class="k"&gt;in &lt;/span&gt;0.3s
//exp_python/webapp:test                                                 PASSED &lt;span class="k"&gt;in &lt;/span&gt;0.7s

Executed 2 out of 2 tests: 2 tests pass.
There were tests whose specified size is too big. Use the &lt;span class="nt"&gt;--test_verbose_timeout_warnings&lt;/span&gt; &lt;span class="nb"&gt;command &lt;/span&gt;line option to see which ones these aINFO: Build completed successfully, 3 total actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seems to work, try to introduce issue format issue (like previously) to check  if detected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ bazel &lt;span class="nb"&gt;test&lt;/span&gt; //exp_python/...
INFO: Analyzed 4 targets &lt;span class="o"&gt;(&lt;/span&gt;0 packages loaded, 0 targets configured&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
INFO: Found 2 targets and 2 &lt;span class="nb"&gt;test &lt;/span&gt;targets...
FAIL: //exp_python/webapp:check &lt;span class="o"&gt;(&lt;/span&gt;see /home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/execroot/__main__/bazel-out/k8-fastbuild/testlogs/exp_python/webapp/check/test.log&lt;span class="o"&gt;)&lt;/span&gt;
INFO: From Testing //exp_python/webapp:check:
&lt;span class="o"&gt;====================&lt;/span&gt; Test output &lt;span class="k"&gt;for&lt;/span&gt; //exp_python/webapp:check:
/home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/run.py wasn&lt;span class="s1"&gt;'t modified on disk since last run.
/home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/test.py wasn'&lt;/span&gt;t modified on disk since last run.
would reformat /home/david/src/github.com/davidB/sandbox_bazel/exp_python/webapp/main.py
Oh no! 💥 💔 💥
1 file would be reformatted, 2 files would be left unchanged.
&lt;span class="o"&gt;================================================================================&lt;/span&gt;
INFO: Elapsed &lt;span class="nb"&gt;time&lt;/span&gt;: 0.907s, Critical Path: 0.78s
INFO: 3 processes: 4 linux-sandbox.
INFO: Build completed, 1 &lt;span class="nb"&gt;test &lt;/span&gt;FAILED, 3 total actions
//exp_python/webapp:test                                                 PASSED &lt;span class="k"&gt;in &lt;/span&gt;0.7s
//exp_python/webapp:check                                                FAILED &lt;span class="k"&gt;in &lt;/span&gt;0.3s
  /home/david/.cache/bazel/_bazel_david/76e87152cc51687aee6e05b5bdcf89aa/execroot/__main__/bazel-out/k8-fastbuild/testlogs/exp_python/webapp/check/test.log

Executed 2 out of 2 tests: 1 &lt;span class="nb"&gt;test &lt;/span&gt;passes and 1 fails locally.
There were tests whose specified size is too big. Use the &lt;span class="nt"&gt;--test_verbose_timeout_warnings&lt;/span&gt; &lt;span class="nb"&gt;command &lt;/span&gt;line option to see which ones these aINFO: Build completed, 1 &lt;span class="nb"&gt;test &lt;/span&gt;FAILED, 3 total actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🎉 issue is detected.&lt;/p&gt;

&lt;p&gt;After reading the documentation for the &lt;a href="https://docs.bazel.build/versions/4.0.0/skylark/rules.html#configurations"&gt;&lt;code&gt;cfg&lt;/code&gt;&lt;/a&gt;, we thing that the &lt;code&gt;target&lt;/code&gt; is maybe a better value (simply because python always mixe all dependencies (tools, test, runtime,...) it's crappy but it's is way to work). So we now have &lt;code&gt;tools/python_check/defs.bzl&lt;/code&gt; like&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="s"&gt;"""Launch the python_check tool"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_python_check_test_impl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO find a better way to build the command/script and to escape/enclose args
&lt;/span&gt;    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"--check"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"%s '%s'"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;short_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"' '"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Write the file, it is executed by 'bazel test'.
&lt;/span&gt;    &lt;span class="c1"&gt;# ctx.outputs.executable is the default value for DefaultInfo.executable
&lt;/span&gt;    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# To ensure the files needed by the command are available, we put them in
&lt;/span&gt;    &lt;span class="c1"&gt;# the runfiles.
&lt;/span&gt;    &lt;span class="n"&gt;runfiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runfiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;srcs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runfiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runfiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DefaultInfo&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;default_runfiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DefaultInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;runfiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runfiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="n"&gt;python_check_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_python_check_test_impl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"srcs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allow_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;executable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"//tools/python_check"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;We also remove the &lt;a href="https://docs.bazel.build/versions/master/be/general.html#test_suite"&gt;test_suite&lt;/a&gt; from &lt;code&gt;exp_python/webapp/BUILD.bazel&lt;/code&gt; to simplify.&lt;/p&gt;

&lt;p&gt;On more thing before the pause, we can enable CI to run test in &lt;code&gt;.github/workflows/ci.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="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&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;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# virtual environments: https://github.com/actions/virtual-environments&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-20.04&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Caches and restores the bazelisk download directory, the bazel build directory.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache bazel&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v2.1.4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;~/.cache/bazelisk&lt;/span&gt;
            &lt;span class="s"&gt;~/.cache/bazel&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-bazel-cache&lt;/span&gt;

      &lt;span class="c1"&gt;# Checks-out your repository under $GITHUB_WORKSPACE, which is the CWD for&lt;/span&gt;
      &lt;span class="c1"&gt;# the rest of the steps&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@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run the test&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;bazel test //...&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build the code&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;bazel build //...&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  To be continued
&lt;/h2&gt;

&lt;p&gt;It's not the end, we'll continue to setup linter and other stuff, but we're in a state enough to pause (it'was a long article again).&lt;/p&gt;

&lt;p&gt;The sandbox_bazel is hosted on github (not with the same history, due to errors), use tag to have the expected view at end of article: &lt;a href="https://github.com/davidB/sandbox_bazel/tree/article/5_python_2"&gt;article/5_python_2&lt;/a&gt;. I'll be happy to have your comments on this article, or to discuss on github repo.&lt;/p&gt;

</description>
      <category>bazel</category>
      <category>python</category>
      <category>lint</category>
    </item>
  </channel>
</rss>
