<?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: Petr Švihlík</title>
    <description>The latest articles on DEV Community by Petr Švihlík (@petrsvihlik).</description>
    <link>https://dev.to/petrsvihlik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F134598%2F81603489-c6fa-4933-b93a-f04adc4f6657.png</url>
      <title>DEV Community: Petr Švihlík</title>
      <link>https://dev.to/petrsvihlik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/petrsvihlik"/>
    <language>en</language>
    <item>
      <title>Lessons Learned: Migrating from AppVeyor to GitHub Actions</title>
      <dc:creator>Petr Švihlík</dc:creator>
      <pubDate>Fri, 05 Mar 2021 21:26:01 +0000</pubDate>
      <link>https://dev.to/petrsvihlik/lessons-learned-migrating-from-appveyor-to-github-actions-1gh8</link>
      <guid>https://dev.to/petrsvihlik/lessons-learned-migrating-from-appveyor-to-github-actions-1gh8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://petrsvihlik.com/posts/lessons-learned-migrating-from-appveyor-to-github-actions" rel="noopener noreferrer"&gt;petrsvihlik.com&lt;/a&gt; — that's where this post is kept up to date.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fluf7v7vp6sq9ed9v5de2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fluf7v7vp6sq9ed9v5de2.png" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The third article about GitHub Actions in a row? One must be thinking I am up to something. And they'd be right. Over the past week, I migrated more than 10 repositories from AppVeyor to GitHub Actions.&lt;/p&gt;

&lt;p&gt;"Why?" you are asking? My team did extensive research on which CI provider to use next as we started hitting some limits with Travis and we weren't happy with how scattered our build procedures were over multiple CIs. We've been using a different CI provider for nearly every stack. Considering the number of SDKs that we provide (JavaScript, .NET, PHP, Java, Swift, Android, Ruby), it should come as no surprise that we wanted to reduce the maintenance required. And we decided that GitHub Actions are simply the best to fit our needs, closely followed by CircleCI.&lt;/p&gt;

&lt;p&gt;Over the course of the migration, I ran into multiple issues. Most of them are pretty easy to solve (at least if you pay attention to the documentation) so the intention of this article is not to give you solutions to simple problems but rather to give you an overview of what types of problems you might face along the way and how much time (based on the knowledge of your codebase) is required to convert a repo to GH Actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linux vs. Windows
&lt;/h2&gt;

&lt;p&gt;By default, all actions run on Linux by default. And if possible, it's a good idea to keep it that way as it's the least resource-demanding option (which you'll also learn if you check out &lt;a href="https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions" rel="noopener noreferrer"&gt;the minute multiplier&lt;/a&gt; on the pricing page).&lt;br&gt;
Another reason to stick to Linux is that, if you use Windows for local development, you have verification of cross-platform compatibility. Obviously, you can also use &lt;a href="https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix" rel="noopener noreferrer"&gt;the strategy matrix&lt;/a&gt; to run multiple platforms at once.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;PRO TIP: For more complex build scripts, it's a good idea to take an iterative approach and first migrate to &lt;code&gt;windows-latest&lt;/code&gt;, make sure everything works, and then switch to &lt;code&gt;ubuntu-latest&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Case sensitivity
&lt;/h3&gt;

&lt;p&gt;As you know, on Linux, the file system is case-sensitive. So beware of files like &lt;code&gt;Directory.Build.props&lt;/code&gt;. Yeah, &lt;code&gt;Directory.build.props&lt;/code&gt; is something completely different.&lt;/p&gt;
&lt;h3&gt;
  
  
  Directory separator
&lt;/h3&gt;

&lt;p&gt;Some of your code or unit tests may depend on the file system. In that case, make sure you assemble all paths using &lt;code&gt;Path.Combine()&lt;/code&gt; or &lt;code&gt;Path.DirectorySeparatorChar&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Quotes
&lt;/h3&gt;

&lt;p&gt;Some shell commands require different syntax than on Windows. A nice example is &lt;code&gt;dotnet nuget push&lt;/code&gt; which requires the first parameter to be &lt;a href="https://github.com/dotnet/docs/issues/7146#issuecomment-604057410" rel="noopener noreferrer"&gt;enclosed in apostrophes&lt;/a&gt; on Linux to properly parse the wildcard when publishing multiple NuGet packages.&lt;/p&gt;
&lt;h3&gt;
  
  
  Running PowerShell
&lt;/h3&gt;

&lt;p&gt;Yes, you can run PowerShell Core on Linux, which is absolutely brilliant if you're migrating from Windows and want to keep just one set of scripts!&lt;br&gt;
The only thing you need to do is to &lt;a href="https://docs.github.com/en/actions/reference/encrypted-secrets#example-using-powershell" rel="noopener noreferrer"&gt;specify &lt;code&gt;pwsh&lt;/code&gt;&lt;/a&gt; as the type of your shell.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pwsh&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;SUPER_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SuperSecret }}&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;example-command "$env:SUPER_SECRET"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just notice the different syntax for accessing environment variables - &lt;code&gt;$SUPER_SECRET&lt;/code&gt; in Bash vs &lt;code&gt;$env:SUPER_SECRET&lt;/code&gt; in PowerShell and don't mistake the &lt;code&gt;pwsh&lt;/code&gt; moniker with &lt;code&gt;powershell&lt;/code&gt; which is the non-Core version of PowerShell supported only on Windows.&lt;br&gt;
See the shell platform compatibility matrix &lt;a href="https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  WSL is your best friend
&lt;/h3&gt;

&lt;p&gt;If you don't have the Windows Subsystem for Linux installed, or worse, you don't know what it is, I encourage you to head to the &lt;a href="https://docs.microsoft.com/en-us/windows/wsl/install-win10" rel="noopener noreferrer"&gt;docs&lt;/a&gt; and be amazed. Being able to execute any Linux command on Windows is, in my opinion, one of the most brilliant engineering achievements of Microsoft in the past years.&lt;br&gt;
Personally, I'm a fan of &lt;a href="https://www.microsoft.com/en-us/p/opensuse-leap-152/9mzd0n9z4m4h" rel="noopener noreferrer"&gt;openSUSE&lt;/a&gt; but you can choose from many distros.&lt;/p&gt;
&lt;h3&gt;
  
  
  HttpClient
&lt;/h3&gt;

&lt;p&gt;The HTTP request and response objects look slightly different when you serialize them on Windows and Linux. If, for instance, your unit tests heavily depend on serialized responses, you might want to address this issue or choose to run the workflow on &lt;code&gt;windows-latest&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;.csproj&lt;/code&gt; patching
&lt;/h2&gt;

&lt;p&gt;AppVeyor offers features such as &lt;a href="https://www.appveyor.com/docs/build-configuration/#net-core-csproj-files-patching" rel="noopener noreferrer"&gt;&lt;code&gt;.csproj&lt;/code&gt; files patching&lt;/a&gt; and &lt;a href="https://www.appveyor.com/docs/build-configuration/#assemblyinfo-patching" rel="noopener noreferrer"&gt;&lt;code&gt;AssemblyInfo&lt;/code&gt; patching&lt;/a&gt;.&lt;br&gt;
These features are not available in GitHub Actions and the easiest way to overcome this is to use parameters that the .NET CLI offers &lt;a href="https://github.com/Kentico/kontent-delivery-sdk-net/blob/ec9ad537199f88a2f88a7c6545a7aeec8d7cbe39/.github/workflows/release.yml#L24" rel="noopener noreferrer"&gt;&lt;code&gt;dotnet build -p:Version=1.2.3&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/Kentico/kontent-delivery-sdk-net/blob/ec9ad537199f88a2f88a7c6545a7aeec8d7cbe39/.github/workflows/release.yml#L30" rel="noopener noreferrer"&gt;&lt;code&gt;dotnet pack -p:PackageVersion=1.2.3&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://github.com/Kentico/kontent-boilerplate-net/blob/8afced3a3d537e39372e7e84f49921d3057f5a33/.github/workflows/release.yml#L30" rel="noopener noreferrer"&gt;&lt;code&gt;dotnet pack -p:NuspecProperties="Version=1.2.3"&lt;/code&gt;&lt;/a&gt; if you use a &lt;code&gt;.nuspec&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;This is how it can look like in a real workflow:&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;Extract version from tag&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;get_version&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;battila7/get-version-action@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;Build&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet build /p:Version="${{ steps.get_version.outputs.version-without-v }}"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pack&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;dotnet pack /p:PackageVersion="${{ steps.get_version.outputs.version-without-v }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Symbols
&lt;/h2&gt;

&lt;p&gt;If you want to enable &lt;a href="https://github.com/dotnet/sourcelink" rel="noopener noreferrer"&gt;Source Link&lt;/a&gt; for the consumers of your NuGets, make sure you turn on deterministic builds using &lt;code&gt;dotnet build /p:ContinuousIntegrationBuild=true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using custom actions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Actions use &lt;a href="https://semver.org/" rel="noopener noreferrer"&gt;Semantic Versioning&lt;/a&gt; so you don't have to include the minor and patch version (&lt;code&gt;codecov/codecov-action@v1.2.3&lt;/code&gt;) when referencing them. It's actually a good idea to use just the major version (&lt;code&gt;codecov/codecov-action@v1&lt;/code&gt;) because you get any potential fixes free of charge.&lt;/li&gt;
&lt;li&gt;You can use actions that are not published on the Marketplace. For instance, you can create your own actions and refer the them by &lt;code&gt;gh_username/repo_with_action@version&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Creating custom actions
&lt;/h2&gt;

&lt;p&gt;Two things I learned about creating custom GitHub Actions that you might find asking yourself too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's impossible to nest GitHub Actions (create GitHub Actions containing other GitHub Actions). However, according to &lt;a href="https://github.com/actions/runner/issues/646#issuecomment-788373907" rel="noopener noreferrer"&gt;this&lt;/a&gt;, it seems the topic of templating is about to be addressed this year.&lt;/li&gt;
&lt;li&gt;Hosting multiple actions in a single repo IS &lt;a href="https://stackoverflow.com/questions/59259783/how-to-combine-two-action-yml-files-in-one-repo" rel="noopener noreferrer"&gt;possible&lt;/a&gt;. You then refer to such actions as  &lt;code&gt;gh_username/repo_with_actions/action1@version&lt;/code&gt;. I'm not sure if it's possible to &lt;a href="https://docs.github.com/en/actions/creating-actions/publishing-actions-in-github-marketplace" rel="noopener noreferrer"&gt;publish&lt;/a&gt; them to the marketplace in this setup though.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;And that's it. There we no other issues during the migration. That was a great surprise for me. Learning GitHub Actions and migrating most of the repos took me a single weekend. Everything was documented properly, I found many examples, and didn't run to any bugs!&lt;/p&gt;

&lt;p&gt;Good job GitHub Actions!&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>appveyor</category>
      <category>ci</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Using Environment Protection Rules to Secure Secrets When Building External Forks with pull_request_target</title>
      <dc:creator>Petr Švihlík</dc:creator>
      <pubDate>Fri, 05 Mar 2021 19:13:05 +0000</pubDate>
      <link>https://dev.to/petrsvihlik/using-environment-protection-rules-to-secure-secrets-when-building-external-forks-with-pullrequesttarget-hci</link>
      <guid>https://dev.to/petrsvihlik/using-environment-protection-rules-to-secure-secrets-when-building-external-forks-with-pullrequesttarget-hci</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://petrsvihlik.com/posts/using-environment-protection-rules-to-secure-secrets-when-building-external-forks-with-pull-request-target" rel="noopener noreferrer"&gt;petrsvihlik.com&lt;/a&gt; — that's where this post is kept up to date.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article is from March 2021 and describes &lt;code&gt;pull_request_target&lt;/code&gt; as it worked back then. GitHub changed the event's semantics in December 2025, and &lt;code&gt;actions/checkout&lt;/code&gt; now refuses the exact checkout shown below by default. The pattern's core idea — a human approval gate in front of secrets — survived and is now semi-official practice, but don't copy the YAML verbatim. The postscript tells the whole story.&lt;/p&gt;
&lt;/blockquote&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffo03wnaz9k2mlox1gjze.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffo03wnaz9k2mlox1gjze.png" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building pull requests from forked repositories with GitHub Actions can be a bit tricky when it comes to secrets. As per the &lt;a href="https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, with the exception of &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, secrets are not passed to the runner when a workflow is triggered from a forked repository. This is to prevent the automatic execution of untrusted code that may be contained within the forked repo.&lt;br&gt;
In other words, we can't use the &lt;code&gt;pull_request&lt;/code&gt; trigger if there are secrets that need to be involved in the workflow.&lt;/p&gt;

&lt;p&gt;Fortunately, &lt;code&gt;pull_request_target&lt;/code&gt; &lt;a href="https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks" rel="noopener noreferrer"&gt;comes to rescue&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;...the &lt;code&gt;pull_request_target&lt;/code&gt; event behaves in an almost identical way to the pull_request event with the same set of filters and payload. However, instead of running against the workflow and code from the merge commit, the event runs against the workflow and code from the base of the pull request. This means the workflow is running from a trusted source...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ok, so now we have access to secrets but we're building the wrong code.&lt;/p&gt;

&lt;p&gt;Apparently, there are some people who try to overcome this problem with the following code:&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;#INSECURE&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@v2&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;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt; &lt;span class="c1"&gt;# Check out the code of the PR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is highly discouraged and rightfully so, as it's insecure if no other security measures are taken.&lt;/p&gt;

&lt;p&gt;In GitHub's own article &lt;a href="https://securitylab.github.com/research/github-actions-preventing-pwn-requests" rel="noopener noreferrer"&gt;Preventing pwn requests&lt;/a&gt;, the author - &lt;a href="https://blog.devsecurity.eu/" rel="noopener noreferrer"&gt;Jaroslav Lobačevski&lt;/a&gt; - suggests using the &lt;code&gt;pull_request_target&lt;/code&gt; in combination with a condition checking whether the PR is labeled &lt;code&gt;safe to test&lt;/code&gt;. Like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and test&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;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;contains(github.event.pull_request.labels.*.name, 'safe to test')&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a perfectly valid approach but I think I may have found a better and more convenient way of preventing unauthorized code execution during the build of forks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment protection rules
&lt;/h2&gt;

&lt;p&gt;Just a couple of months ago, GitHub introduced the &lt;a href="https://github.blog/changelog/2020-12-15-github-actions-environments-environment-protection-rules-and-environment-secrets-beta/" rel="noopener noreferrer"&gt;Environment protection rules&lt;/a&gt;. The main intent of this feature is to protect environments during deployments by applying rules that will pause the execution of a workflow until given conditions are met - e.g. a human approval is given, the certain time elapsed, etc. But it can serve any general purpose. In our case, we'll use it to protect our repository secrets and to prevent the execution of untrusted code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Protecting the build
&lt;/h3&gt;

&lt;p&gt;Let's start with adding a dummy environment called "Integrate Pull Request" that will require human approval.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ahrfctml0l3jmiqt5od.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ahrfctml0l3jmiqt5od.png" alt="Integrate Pull Request Environment" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our main build procedure will be associated with this environment and preceded by a dummy workflow step &lt;code&gt;approve&lt;/code&gt; that will kick off the workflow and inform the author of the pull request that a review is necessary before proceeding any further.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request_target&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;master&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;approve&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# First step&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;Approve&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo For security reasons, all pull requests need to be approved first before running any automated CI.&lt;/span&gt;

  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Second step&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;approve&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# Require the first step to finish&lt;/span&gt;
    &lt;span class="na"&gt;environment&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;Integrate Pull Request&lt;/span&gt; &lt;span class="c1"&gt;# Our dummy environment&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="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way the workflow won't proceed until someone reviews the submitted code and therefore, we can safely check out the &lt;code&gt;${{ github.event.pull_request.head.sha }}&lt;/code&gt; in the next step and build it. So the build is executed using a trusted workflow from the base of the PR and the actual code of the PR.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works in practice
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Someone submits a pull request and a workflow is triggered and immediately paused&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmbx2yam6cje5stb1phym.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmbx2yam6cje5stb1phym.png" alt="Build is waiting for human approval" width="725" height="467"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The reviewer or a group of reviewers receive an e-mail notification&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6voac0yby88e8dzraq7j.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6voac0yby88e8dzraq7j.png" alt="Email notification about a pending review" width="529" height="326"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The reviewer clicks the link, navigates to the repo, verifies that the submitted PR doesn't contain any unwanted code, and finally gives an approval&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqq3hke20ub1v39u2h0ec.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqq3hke20ub1v39u2h0ec.png" alt="Approve the workflow step" width="616" height="396"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The build proceeds&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi3yv8i4254y7l9jnakl6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi3yv8i4254y7l9jnakl6.png" alt="Build is approved" width="713" height="401"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All approvals are audited&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tojt68b3xggsmrb2tbp.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tojt68b3xggsmrb2tbp.png" alt="Approval audit log" width="798" height="189"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A few words on Codecov
&lt;/h2&gt;

&lt;p&gt;While implementing this workflow, I ran into an issue where the Codecov action, similarly to the &lt;a href="https://github.com/marketplace/actions/checkout" rel="noopener noreferrer"&gt;GitHub Checkout Action&lt;/a&gt;, is by default &lt;a href="https://github.com/codecov/codecov-action/issues/155" rel="noopener noreferrer"&gt;pointed to the PR's Base&lt;/a&gt; and needs to be overridden. This can be achieved by:&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;Codecov&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;codecov/codecov-action@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;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CODECOV_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;override_pr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.number }}&lt;/span&gt;
    &lt;span class="na"&gt;override_commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get rid of the following warning message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Issue detecting commit SHA. Please run actions/checkout with fetch-depth &amp;gt; 1 or set to 0&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;make sure to also set &lt;code&gt;fetch-depth&lt;/code&gt; of the Checkout action to 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&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;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: I found an alternative approach using a conditional &lt;a href="https://github.com/Cookie-AutoDelete/Cookie-AutoDelete/actions/runs/274505391/workflow" rel="noopener noreferrer"&gt;workflow step&lt;/a&gt; and a &lt;a href="https://github.com/Cookie-AutoDelete/Cookie-AutoDelete/blob/3.X.X-Branch/.github/codecov_alt.sh" rel="noopener noreferrer"&gt;shell script&lt;/a&gt;. But overriding the commit SHA is far easier.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;The advantage of this approach is that you can assign a group of reviewers who'll receive an email notification about the pending workflow and can review and approve it in a single click.&lt;br&gt;
The process is, in my opinion, more transparent thanks to all events being logged and semantically more correct than using labels.&lt;/p&gt;

&lt;p&gt;If you want to explore the whole workflow, feel free to check out my project &lt;a href="https://github.com/petrsvihlik/WopiHost/blob/master/.github/workflows/pull_request.yml" rel="noopener noreferrer"&gt;WopiHost&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To learn more about the specifics of &lt;code&gt;pull_request_target&lt;/code&gt; head to the &lt;a href="https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Postscript (August 2026): what four years did to this pattern
&lt;/h2&gt;

&lt;p&gt;I wrote everything above in March 2021, when environments were a few months old and &lt;code&gt;pull_request_target&lt;/code&gt; was the shiny new answer to fork builds. Publishing security advice comes with an implicit contract: you should come back later and tell people how it aged. Here's my report, and it's a story in four acts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Act one: the pattern spreads
&lt;/h3&gt;

&lt;p&gt;The human-approval-gate idea took off. Variants of "gate the secrets behind an environment with required reviewers" appeared in community guidance and eventually in &lt;a href="https://docs.github.com/en/actions/reference/security/secure-use" rel="noopener noreferrer"&gt;GitHub's own documentation on securely using &lt;code&gt;pull_request_target&lt;/code&gt;&lt;/a&gt;. For a while it was fair to call it semi-official best practice, and I'll admit the 2021 me would have been pretty pleased with that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Act two: the bypass (February 2025)
&lt;/h3&gt;

&lt;p&gt;Then researchers at QuantCo found a hole — not in my workflow specifically, but in the ground it stood on. In &lt;a href="https://tech.quantco.com/blog/github-actions-environments/" rel="noopener noreferrer"&gt;Pull Requests Go Both Ways&lt;/a&gt;, Yannik Tausch and Oliver Borchert showed that &lt;strong&gt;any collaborator with push access to any branch could bypass an environment's deployment-branch restriction&lt;/strong&gt; and reach its protected secrets and OIDC tokens by leveraging &lt;code&gt;pull_request_target&lt;/code&gt; — and, remarkably, a repository was exposed &lt;em&gt;even if none of its workflows used the trigger at all&lt;/em&gt;, because the attacker could introduce a workflow that did. They reported it through GitHub's bug bounty program on HackerOne; GitHub fixed it in December 2025.&lt;/p&gt;

&lt;p&gt;Read that again: the mechanism I recommended for protecting secrets was, for a window of time, itself a way around a related protection. The approval gate still required a human click, but the environment model underneath had a seam nobody had noticed for years.&lt;/p&gt;

&lt;h3&gt;
  
  
  Act three: GitHub rewrites the event (December 2025)
&lt;/h3&gt;

&lt;p&gt;GitHub's fix went deeper than patching the bypass. As of December 8, 2025, &lt;a href="https://github.blog/changelog/2025-11-07-actions-pull_request_target-and-environment-branch-protections-changes/" rel="noopener noreferrer"&gt;the semantics of &lt;code&gt;pull_request_target&lt;/code&gt; changed outright&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The workflow file and checkout now &lt;strong&gt;always come from the repository's default branch&lt;/strong&gt;, no matter which branch the PR targets. &lt;code&gt;GITHUB_REF&lt;/code&gt; resolves to the default branch and &lt;code&gt;GITHUB_SHA&lt;/code&gt; to its latest commit. (Previously, any base branch could supply the workflow — which meant outdated, already-"fixed" vulnerable workflows on stale branches could still be executed. That class of bug is now dead.)&lt;/li&gt;
&lt;li&gt;Environment branch-protection rules are evaluated against the &lt;em&gt;executing&lt;/em&gt; ref — for &lt;code&gt;pull_request_target&lt;/code&gt; that's the default branch, for the &lt;code&gt;pull_request&lt;/code&gt; family it's &lt;code&gt;refs/pull/N/merge&lt;/code&gt; — closing the seam QuantCo found.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the block quote near the top of this article — "the event runs against the workflow and code from the base of the pull request" — is no longer true. It describes an event that doesn't exist anymore.&lt;/p&gt;

&lt;h3&gt;
  
  
  Act four: my checkout line gets refused (June 2026)
&lt;/h3&gt;

&lt;p&gt;The final twist is my favorite. In June 2026, &lt;a href="https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout/" rel="noopener noreferrer"&gt;&lt;code&gt;actions/checkout&lt;/code&gt; v7 started refusing to fetch fork PR code in &lt;code&gt;pull_request_target&lt;/code&gt; workflows by default&lt;/a&gt; — and among the "insecure inputs" it now rejects is, verbatim, &lt;code&gt;ref: ${{ github.event.pull_request.head.sha }}&lt;/code&gt;. The exact line this article is built around. The enforcement was even backported to older checkout versions.&lt;/p&gt;

&lt;p&gt;There's an opt-out for workflows that check out fork code deliberately, with elevated trust — which is precisely what the approval-gate pattern does. It's called &lt;code&gt;allow-unsafe-pr-checkout&lt;/code&gt;, and GitHub says the name is intentionally ugly so it stands out in code review. I find that genuinely elegant: the platform now forces the 2021 pattern to &lt;em&gt;declare itself&lt;/em&gt; instead of blending into innocent-looking YAML.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd actually do today
&lt;/h3&gt;

&lt;p&gt;The idea survived; the YAML didn't. If I were setting this up in 2026:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep the environment with &lt;strong&gt;required reviewers&lt;/strong&gt; in front of any job that touches secrets — that part aged well, and it's why the opt-out exists at all.&lt;/li&gt;
&lt;li&gt;Accept the new semantics: your workflow always runs from the default branch. That's a feature — fix a vulnerable workflow once, on the default branch, and stale branches can't resurrect it.&lt;/li&gt;
&lt;li&gt;Check out the fork's code only in the gated job, with &lt;code&gt;allow-unsafe-pr-checkout&lt;/code&gt; set — visibly, greppably, deliberately.&lt;/li&gt;
&lt;li&gt;Scope the secrets to the bare minimum the fork build needs, and prefer OIDC over long-lived credentials.&lt;/li&gt;
&lt;li&gt;Re-read &lt;a href="https://docs.github.com/en/actions/reference/security/secure-use" rel="noopener noreferrer"&gt;GitHub's hardening guidance&lt;/a&gt; before trusting anything — including this post.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The meta-lesson
&lt;/h3&gt;

&lt;p&gt;Security patterns have a half-life, and it's shorter than the half-life of blog posts about them. This article ranked in searches for years after the ground truth underneath it had shifted — which is exactly how "semi-canonical practice" quietly becomes a liability. The 2021 idea was right: put a human between untrusted code and your secrets. Everything else — the event semantics, the environment model, even the checkout line — turned out to be rented ground.&lt;/p&gt;

&lt;p&gt;I'd rather amend my old advice in public than have it silently rot in a search index. Consider this the amendment.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>pullrequest</category>
      <category>security</category>
      <category>ci</category>
    </item>
    <item>
      <title>Reporting .NET 5 XUnit Code Coverage in Codecov via GitHub Actions and Coverlet</title>
      <dc:creator>Petr Švihlík</dc:creator>
      <pubDate>Fri, 05 Mar 2021 14:56:10 +0000</pubDate>
      <link>https://dev.to/kontent_ai/reporting-net-5-xunit-code-coverage-in-codecov-via-github-actions-and-coverlet-4h5i</link>
      <guid>https://dev.to/kontent_ai/reporting-net-5-xunit-code-coverage-in-codecov-via-github-actions-and-coverlet-4h5i</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://petrsvihlik.com/posts/reporting-net-5-xunit-code-coverage-in-codecov-via-github-actions-and-coverlet" rel="noopener noreferrer"&gt;petrsvihlik.com&lt;/a&gt; — that's where this post is kept up to date.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Written for .NET 5 in 2021 and archived as-is: the concepts hold, but the commands have moved on (coverlet's collector with &lt;code&gt;--collect:"XPlat Code Coverage"&lt;/code&gt; is the current path, and the Codecov action now requires a token). Originally published on the &lt;a href="https://dev.to/kontent_ai/reporting-net-5-xunit-code-coverage-in-codecov-via-github-actions-and-coverlet-4h5i"&gt;Kontent.ai dev.to blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuwrvziusol63lhtn1so5.webp" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuwrvziusol63lhtn1so5.webp" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You might remember my &lt;a href="https://dev.to/petrsvihlik/running-net-core-3-xunit-code-coverage-in-appveyor-using-opencover-and-codecov-1n7p"&gt;earlier post&lt;/a&gt; where I described how to set up code coverage reporting for .NET Core 3 using AppVeyor + OpenCover + Codecov.&lt;/p&gt;

&lt;p&gt;Time has moved on, .NET 5 has arrived, and the best practices have changed, so let's see how to collect code coverage for .NET projects in 2021.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;For some time, I have been intrigued by the &lt;code&gt;coverlet.collector&lt;/code&gt; that you can find in any new XUnit &lt;code&gt;.csproj&lt;/code&gt;. What it is? How can I use it? Can I replace OpenCover with it?&lt;/p&gt;

&lt;p&gt;Also, I started noticing people migrating to GitHub Actions and I didn't have any experience with it and kinda felt that I'm missing the train. This observation has been confirmed recently in the &lt;a href="https://about.codecov.io/resource/2020-state-of-open-source-code-coverage/" rel="noopener noreferrer"&gt;2020 State of Open Source Code Coverage&lt;/a&gt; report where GitHub Actions scored the #1 fastest growing CI used with Codecov (and also the #1 in absolute numbers).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://about.codecov.io/resource/2020-state-of-open-source-code-coverage/#sos-23" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb5o88x6dgkxl88gp8h6q.jpg" alt="2020 State of Open Source Code Coverage" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was thinking I could modernize the stack that I use for .NET projects both at work and for my personal projects. Of the three tools that I've been using previously, I'll be keeping just Codecov (because it's awesome ❤). Let's get to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating coverage
&lt;/h2&gt;

&lt;p&gt;I like to use the &lt;code&gt;dotnet&lt;/code&gt; CLI for as many tasks as possible. The good news is that &lt;code&gt;coverlet&lt;/code&gt; offers deep integration with &lt;code&gt;msbuild&lt;/code&gt;.&lt;br&gt;
This is how you can generate code coverage for the whole solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/p:CollectCoverage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/p:CoverletOutputFormat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;opencover&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command will generate &lt;code&gt;coverage.opencover.xml&lt;/code&gt; files in all your test projects (if you run &lt;code&gt;dotnet test&lt;/code&gt; on the solution level).&lt;/p&gt;

&lt;p&gt;All you need to do to make the code coverage-related &lt;code&gt;/p:&lt;/code&gt; switches work is to install also the &lt;a href="https://www.nuget.org/packages/coverlet.msbuild/" rel="noopener noreferrer"&gt;&lt;code&gt;coverlet.msbuild&lt;/code&gt;&lt;/a&gt; NuGet package. So your &lt;code&gt;.csproj&lt;/code&gt;s will look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"coverlet.collector"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"3.0.3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;IncludeAssets&amp;gt;&lt;/span&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;span class="nt"&gt;&amp;lt;/IncludeAssets&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PrivateAssets&amp;gt;&lt;/span&gt;all&lt;span class="nt"&gt;&amp;lt;/PrivateAssets&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PackageReference&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"coverlet.msbuild"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"3.0.3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PrivateAssets&amp;gt;&lt;/span&gt;all&lt;span class="nt"&gt;&amp;lt;/PrivateAssets&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;IncludeAssets&amp;gt;&lt;/span&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;span class="nt"&gt;&amp;lt;/IncludeAssets&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PackageReference&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to leave in the &lt;code&gt;/p:CoverletOutputFormat=opencover&lt;/code&gt; switch as this is the format that's accepted by Codecov.&lt;/p&gt;

&lt;p&gt;For more advanced configuration, I recommend reading the documentation on &lt;a href="https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/MSBuildIntegration.md" rel="noopener noreferrer"&gt;Coverlet's integration with MSBuild&lt;/a&gt;. This is especially useful when your solution or projects don't stick to naming conventions and you need to do some extra filtering, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating into GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Plugging the &lt;code&gt;dotnet test&lt;/code&gt; into the CI pipeline is just a matter of creating a new workflow step. No matter if you use the &lt;code&gt;ubuntu-latest&lt;/code&gt; or &lt;code&gt;windows-latest&lt;/code&gt; runner, it works flawlessly.&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;Restore dependencies&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet restore&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet build --no-restore /p:ContinuousIntegrationBuild=true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;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;dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Collecting coverage by Codecov
&lt;/h2&gt;

&lt;p&gt;The last step is to upload the test coverage to Codecov. You can find an official &lt;a href="https://github.com/marketplace/actions/codecov" rel="noopener noreferrer"&gt;Codecov GitHub Action&lt;/a&gt; on the marketplace which makes the process super easy - no need to install the Codecov CLI, etc.&lt;/p&gt;

&lt;p&gt;Just add the following lines to your workflow:&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;Codecov&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;codecov/codecov-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codecov will search through the folder structure of your project and discover your coverage reports based on conventions. You don't even have to include the &lt;code&gt;CODECOV_TOKEN&lt;/code&gt; for public repos.&lt;/p&gt;

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

&lt;p&gt;As you can see, this approach is much more convenient than &lt;a href="https://dev.to/petrsvihlik/running-net-core-3-xunit-code-coverage-in-appveyor-using-opencover-and-codecov-1n7p"&gt;the one I was using before&lt;/a&gt; - downloading and installing tools, running PowerShell scripts, etc.&lt;/p&gt;

&lt;p&gt;If you stick to standard .NET naming and folder structure conventions, setting up code coverage is actually very easy nowadays.&lt;/p&gt;

&lt;p&gt;To see the whole workflow in action, head to some of our &lt;a href="https://github.com/Kentico/kontent-delivery-sdk-net/actions/workflows/integrate.yml" rel="noopener noreferrer"&gt;SDK repos&lt;/a&gt;. You can also explore how we do releases using GitHub Actions. All code is in the &lt;a href="https://github.com/Kentico/kontent-delivery-sdk-net/tree/master/.github/workflows" rel="noopener noreferrer"&gt;&lt;code&gt;workflows folder&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In my next article, I'll cover how to &lt;a href="https://dev.to/petrsvihlik/using-environment-protection-rules-to-secure-secrets-when-building-external-forks-with-pullrequesttarget-hci"&gt;secure secrets when building pull requests from forks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>net5</category>
      <category>coverlet</category>
      <category>codecov</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Running .NET Core 3 XUnit Code Coverage in AppVeyor Using OpenCover and Codecov</title>
      <dc:creator>Petr Švihlík</dc:creator>
      <pubDate>Sun, 01 Dec 2019 21:25:45 +0000</pubDate>
      <link>https://dev.to/petrsvihlik/running-net-core-3-xunit-code-coverage-in-appveyor-using-opencover-and-codecov-1n7p</link>
      <guid>https://dev.to/petrsvihlik/running-net-core-3-xunit-code-coverage-in-appveyor-using-opencover-and-codecov-1n7p</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://petrsvihlik.com/posts/running-net-core-3-xunit-code-coverage-in-appveyor-using-opencover-and-codecov" rel="noopener noreferrer"&gt;petrsvihlik.com&lt;/a&gt; — that's where this post is kept up to date.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tools from 2019, preserved as history: .NET Core 3 is long out of support, OpenCover has been retired by its maintainer, and Codecov's uploader has been replaced since. For the modern approach, see the follow-up: &lt;a href="https://petrsvihlik.com/posts/reporting-net-5-xunit-code-coverage-in-codecov-via-github-actions-and-coverlet" rel="noopener noreferrer"&gt;Reporting .NET 5 XUnit Code Coverage in Codecov via GitHub Actions and Coverlet&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg8m3nmbphbzk4b5u2zuz.webp" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg8m3nmbphbzk4b5u2zuz.webp" width="685" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I love AppVeyor. It's my go-to CI/CD service for .NET projects. I use it for both &lt;a href="https://github.com/petrsvihlik/WopiHost/" rel="noopener noreferrer"&gt;personal&lt;/a&gt; and &lt;a href="https://github.com/Kentico/kontent-delivery-sdk-net" rel="noopener noreferrer"&gt;work&lt;/a&gt; open-source projects. I also love automation of all kinds so, among other things, I want my test coverage reports to be automatically generated with every build.&lt;/p&gt;

&lt;p&gt;Recently, I started upgrading my project portfolio to .NET Core 3.0. With that, I also wanted to level-up my unit tests which are typically XUnit. I didn't want to introduce too much change to my code so, after some research, I decided to stick with the following setup: AppVeyor (which I've already been using) + Codecov (which we use for another project at work) + OpenCover.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom PowerShell coverage script 📜
&lt;/h3&gt;

&lt;p&gt;The goal was to create a minimalistic, reusable piece of code (independent of environment settings, tool versions, etc.) that I could use across multiple projects. Here it goes:&lt;/p&gt;

&lt;h4&gt;
  
  
  coverage.ps1
&lt;/h4&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;The script can run locally and in AppVeyor with slightly different configurations. The most tricky part is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$register = if ($ENV:APPVEYOR -eq $true ) { '-register' } else { '-register:user' }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When I finally made the script run on my local machine, it started failing in AppVeyor. It took me a few hours of digging and debugging to figure out that I can't use &lt;code&gt;-register:user&lt;/code&gt; in AppVeyor. Fortunately, debugging is quite easy in AppVeyor as you can &lt;a href="https://www.appveyor.com/docs/how-to/rdp-to-build-worker/" rel="noopener noreferrer"&gt;RDP to the build worker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I highly recommend reading &lt;a href="https://github.com/opencover/opencover/wiki/Usage" rel="noopener noreferrer"&gt;OpenCover's documentation&lt;/a&gt;. It'll help you understand the syntax of &lt;a href="https://github.com/opencover/opencover/wiki/Usage#notes-on-spaces-in-arguments" rel="noopener noreferrer"&gt;&lt;code&gt;targetArgs&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/opencover/opencover/wiki/Usage#optional-arguments" rel="noopener noreferrer"&gt;&lt;code&gt;filter&lt;/code&gt; and &lt;code&gt;register&lt;/code&gt;&lt;/a&gt; parameters.&lt;/p&gt;
&lt;h3&gt;
  
  
  Hooking the script into the CI pipeline 🔗
&lt;/h3&gt;

&lt;p&gt;AppVeyor supports &lt;a href="https://chocolatey.org/" rel="noopener noreferrer"&gt;Chocolatey&lt;/a&gt; so I'm using &lt;code&gt;cinst&lt;/code&gt; to install OpenCover and Codecov CLIs. Then I'm turning off the default test script and replacing it with my own which runs OpenCover to generate the coverage files, and finally, I'm calling Codecov CLI to upload the results.&lt;/p&gt;

&lt;p&gt;Cool thing is that if you run &lt;code&gt;codecov -f coverage.xml&lt;/code&gt; from AppVeyor you don't need an API key. It just works automagically ✨.&lt;/p&gt;
&lt;h4&gt;
  
  
  appveyor.yml
&lt;/h4&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;h3&gt;
  
  
  The result
&lt;/h3&gt;

&lt;p&gt;I've got this nice sunburst chart indicating which files need attention:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqz45x1gh5sivhzqbexwc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqz45x1gh5sivhzqbexwc.png" alt="Codecov sunburst" width="562" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've also got the Codecov badge with % coverage in the &lt;a href="https://github.com/Kentico/kontent-delivery-sdk-net" rel="noopener noreferrer"&gt;kontent-delivery-sdk-net&lt;/a&gt; repo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Kentico/kontent-delivery-sdk-net" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjodfuxkabxiwdopwaj5e.png" alt="Codecov coverage badge" width="548" height="80"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And last but not least, a nice ASCII art in the AppVeyor log here:&lt;br&gt;
&lt;a href="https://ci.appveyor.com/project/kentico/deliver-net-sdk/branch/master#L310" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0twrs42sd0t94f34huqo.png" alt="Alt Text" width="306" height="125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So far, I have successfully tested this approach in two repos so I hope it'll work for you too. Any improvements are welcome!&lt;/p&gt;

</description>
      <category>netcore</category>
      <category>appveyor</category>
      <category>codecov</category>
      <category>opencover</category>
    </item>
    <item>
      <title>What I Look for When I Read Your CV</title>
      <dc:creator>Petr Švihlík</dc:creator>
      <pubDate>Sun, 25 Aug 2019 22:10:06 +0000</pubDate>
      <link>https://dev.to/petrsvihlik/why-i-won-t-hire-you-5-honest-tips-to-a-better-cv-540g</link>
      <guid>https://dev.to/petrsvihlik/why-i-won-t-hire-you-5-honest-tips-to-a-better-cv-540g</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://petrsvihlik.com/posts/why-i-won-t-hire-you-5-honest-tips-to-a-better-cv" rel="noopener noreferrer"&gt;petrsvihlik.com&lt;/a&gt; — that's where this post is kept up to date.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A version of this article appeared in 2019 as "Why I Won't Hire You — 5 Honest Tips to a Better CV". Rewritten in August 2026: a few hundred CVs later, from the other side of more hiring rounds, and with AI in the mix.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw15uar175olkpa6h67z4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw15uar175olkpa6h67z4.png" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Over the years I've read hundreds of CVs — first hiring developer advocates, these days hiring engineers. Here's the thing that took me embarrassingly long to say out loud: most CVs that don't work fail on the &lt;em&gt;document&lt;/em&gt; level, not the &lt;em&gt;person&lt;/em&gt; level. Behind many a rejected PDF was probably a perfectly capable human whose one page just didn't let them through.&lt;/p&gt;

&lt;p&gt;The document level is fixable in an evening. So instead of telling you what annoys me, let me tell you what works on me — five things, plus what changed now that AI writes half the CVs I see.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. One page is a skill demo
&lt;/h2&gt;

&lt;p&gt;If you can squeeze years of work into one page — two at the absolute maximum — that's a clear sign to me that you know how to work with information: what to keep, what to cut, what matters to whom. That skill happens to be in the job description of every role I've ever hired for, whether we wrote it down or not.&lt;/p&gt;

&lt;p&gt;There's also simple mechanics on my side: a CV gets about ninety seconds of honest attention on the first pass. A tight page puts your best material inside that window. Cutting hurts — but every cut you make is a decision I don't have to, and I notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Ask each line: is it relevant to &lt;em&gt;this&lt;/em&gt; job?
&lt;/h2&gt;

&lt;p&gt;I've read CVs describing burger-flipping at McDonald's and — memorably, true story — the number of graves dug. And look: I understand you worked at McDonald's, and I believe it taught you real things — showing up, handling customers, surviving a rush hour. But is it relevant to the job you're applying to? If it is — it's the main experience you have, or it genuinely taught you something you still use — then frame it exactly that way, and it earns its line. If it isn't, save the story for the interview. I promise I'll ask, and I'll enjoy it more there.&lt;/p&gt;

&lt;p&gt;The same question works for skills. Word and Excel on an IT CV? I already assume you know them — that's precisely why they don't need a line (unless you're the person who builds multi-sheet tax calculators, in which case, tell me more). "10 years of experience in XML"? I'd much rather have one plain paragraph about a system you built with it. Every line that passes the relevance test makes the ones around it more believable.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Show your work — honestly
&lt;/h2&gt;

&lt;p&gt;List what you specialize in, not every technology you've ever brushed against — a short list I can believe beats a long one I can't. Then spend the saved space on the thing that actually informs me: what you built, what &lt;em&gt;your&lt;/em&gt; contribution was, and what came of it. Brief and concrete beats long and impressive. I'd honestly rather read three plain sentences about a project that half-failed — and what you learned — than a wall of superlatives.&lt;/p&gt;

&lt;p&gt;I've also seen elaborate tables breaking down the years and months of experience per programming language, and pie charts and progress bars rating skills out of ten. I get the intent — you want to be measurable. But "C# ▓▓▓▓▓▓▓░░░ 7/10" measured how, compared to whom? A CV is a document, not an infographic (designers and BI folks excepted — that's your portfolio speaking). What the numbers are trying to say, one honest sentence about real work says better — and the interview is where I get to see you think.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqp8bwydwspu606d5m6om.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqp8bwydwspu606d5m6om.png" alt="Intelligence -10" width="164" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Skill bars: the one chart that makes every reader trust you less.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Aim, don't broadcast
&lt;/h2&gt;

&lt;p&gt;Here's the counterintuitive math of applications: ten CVs written &lt;em&gt;to&lt;/em&gt; ten researched companies will outperform a hundred copies of one CV sprayed at a job board. Every experienced reader can tell within seconds which kind landed in their inbox — and a page that was clearly written to &lt;em&gt;us&lt;/em&gt; tells me you chose us, before we ever spoke. That's the strongest motivation signal a document can carry.&lt;/p&gt;

&lt;p&gt;Fewer, better-aimed applications also do something for you: they force you to figure out where you actually want to work before you get there. That clarity leaks into every line of the CV.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Add a cover letter when your path needs a narrator
&lt;/h2&gt;

&lt;p&gt;Switching stacks? Changing roles? Leaving the field you've spent a decade in? Your CV can show the facts of a pivot, but it can't explain the &lt;em&gt;why&lt;/em&gt; — and the why is the whole story. Three honest paragraphs about your motivation turn a confusing CV into a compelling one. When the path is straight, skip it; when the path bends, narrate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI changed — and what it didn't
&lt;/h2&gt;

&lt;p&gt;This rewrite exists partly because the game changed. In 2026, I assume most CVs I read have passed through an LLM, and many were written by one end to end. Two consequences follow, and they point in the same direction.&lt;/p&gt;

&lt;p&gt;First: &lt;strong&gt;polish is now free, so polish is now worthless as a signal.&lt;/strong&gt; Flawless formatting and confident phrasing used to hint at care; today they're the default output. What stands out instead is the thing AI cannot supply: &lt;em&gt;specific, verifiable, first-person detail&lt;/em&gt;. The model doesn't know that your migration cut the build time in half, or that you were the one who talked the team out of the rewrite. Only you can put that on the page — and it now does more work than every adjective combined, precisely because nobody can generate it.&lt;/p&gt;

&lt;p&gt;Second: &lt;strong&gt;the screening got automated too.&lt;/strong&gt; Your CV will likely meet keyword filters and AI screeners before human eyes. The answer isn't to game them — it's the same advice as tip #4: read the job posting carefully and use its actual vocabulary for the things you have honestly done. Tailoring, it turns out, was ATS optimization all along.&lt;/p&gt;

&lt;p&gt;So use AI the way you'd use a sharp editor: let it tighten your sentences, never let it invent your career. Interviews still exist, and the distance between your CV and the person who shows up becomes obvious within about ten minutes. Honesty was always the durable strategy; AI just raised the price of everything else.&lt;/p&gt;




&lt;p&gt;That's it — and the old paradox still holds: the less information you include and the fewer companies you address, the higher your chances of getting hired. Isn't that great?&lt;/p&gt;

&lt;p&gt;One well-aimed, honest page whose goal isn't to list your life — it's to start a conversation. I'm on the reading side again these days: make my ninety seconds easy, and I'll happily spend the next sixty minutes with you instead.&lt;/p&gt;

</description>
      <category>career</category>
      <category>cv</category>
      <category>resume</category>
      <category>coverletter</category>
    </item>
  </channel>
</rss>
