{"id":50524,"date":"2024-02-19T08:30:00","date_gmt":"2024-02-19T16:30:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=50524"},"modified":"2024-02-23T15:25:05","modified_gmt":"2024-02-23T23:25:05","slug":"developing-optimized-github-actions-with-net-and-native-aot","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/developing-optimized-github-actions-with-net-and-native-aot\/","title":{"rendered":"Developing Optimized GitHub Actions with .NET and Native AOT"},"content":{"rendered":"<p>Developing for GitHub Actions has never been easier with .NET. In this post, I&#8217;ll share compelling reasons to author your next GitHub Action with .NET. You&#8217;ll learn how to optimize your .NET container app by using Native AOT. I&#8217;ll also show you how to build and publish your container image to the GitHub Container Registry. I&#8217;m thrilled to share a library that you can use to simplify interacting with GitHub Actions from .NET.<\/p>\n<p>If you&#8217;re more interested in the results without the story of how we got here, you can skip to the <a href=\"#scrutinizing-the-results\">Scrutinizing the results<\/a> section.<\/p>\n<h2>Introduction: A fun use case<\/h2>\n<p>The example application for this article is a profanity filter. Over half a decade ago, I developed a <a href=\"https:\/\/davidpine.net\/blog\/github-profanity-filter\">GitHub profanity filter<\/a> as an Azure function to handle GitHub webhooks. The original implementation was written in C# using .NET Core 2.2, which was later upgraded to .NET Core 3.1. At that time, GitHub Actions were just being introduced, so the project didn&#8217;t utilize them. However, it has been known for some time that you can author .NET apps as GitHub Actions, and official documentation is available to help you get started:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/devops\/create-dotnet-github-action\">Tutorial: Create a GitHub Action with .NET<\/a><\/li>\n<\/ul>\n<p>In this article, I will demonstrate how the original project has evolved into a containerized GitHub Action using .NET 8 and Native AOT.<\/p>\n<h2>The evolution of the profanity filter<\/h2>\n<p>The new <a href=\"https:\/\/github.com\/IEvangelist\/profanity-filter\">profanity filter (named <em>Potty Mouth<\/em>)<\/a> is a .NET app that runs as a GitHub Action. This means that this filter can be used in any repository. The app is a great way to demonstrate the power of .NET and GitHub Actions. The app itself is a .NET console app that&#8217;s published as a container image.<\/p>\n<p>The app maintains several lists of profane content in various languages. It uses these lists to search for matching profane content in GitHub issues and pull requests as they&#8217;re updated. The app is configurable exposing a <a href=\"https:\/\/github.com\/IEvangelist\/profanity-filter?tab=readme-ov-file#-inputs\">set of inputs<\/a>, so you can choose how to handle profane content. For example, you can replace profane content with a series of asterisks or emoji or some other desired strategy.<\/p>\n<p>To help visualize the flow of the profanity filter, consider the following diagram:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2024\/02\/flow-chart.png\" alt=\"Profanity filter: GitHub Action flow chart.\" \/><\/p>\n<p><!--\nMermaid live editor source:\nhttps:\/\/mermaid.live\/edit#pako:eNpdUttu00AQ_ZXRPqAgxVGaWzdGQkqbFoG4VA0SgiQPE3ucrLB33N11Q4iTj0BC4o1n_o5PYGMnass-rHZmzpkzOjtbEXFMIhRJyutohcbBx_FMgz-jRoKLMMFgqdyqWMBrawsCNnBTpCnc0l1B1j2vsQBB8BIuGtME7YEScZaRdkHMzsIla4dKW7gxnKCmKuGrkOABW_VRrOfHXheHXuXnq0kJl40akqjUkYFRnqcbuK6CJ8LlLeUpRgT5USGqFUq4ahwnOqkEkTJRSvDsrmD3wnJGYNeEBtZsYltnYUHRoXCkojG8Doxartxj2t_fP_48Zj2Z6Hp6ssLh0sJbXFAKqnIQdey5v36CIYwcOP5_6vn8oRND-cko51kQk3cxpRhSZR1wAui_wX-ZXpIt4dW0tsriPcEbXsCkyDI0m_mDqVy-_1DCeLs9Ih3nMPHXbgcnwf1-PxJNkZHJUMV-LbaHyky4FWU0E6F_xmi-zsRM7zwOC8eTjY5E6ExBTVHkMToaK1wazIQXSa3PUqwcm3f1nlXr1hQ56i_M2YnoQxFuxTcRdoay1Wt3ZE-2B7Iz6PabYiPCfr_VlYP-oHs-lFJ25HDXFN8rfrs17PTaUvbOPKV73u2d7f4Bo5juDA\n--><\/p>\n<p>The preceding flow chart diagram depicts the following steps:<\/p>\n<ul>\n<li>A GitHub issue or pull request is created, updated or commented on.<\/li>\n<li>If there isn&#8217;t profane content, the action ends.<\/li>\n<li>When it contains profanity, the filter is applied.\n<ul>\n<li>All profane content is replaced with a configured replacement strategy. For this example, imagine that <code>emoji<\/code> is configured.<\/li>\n<li>Also imagine that the word <code>swear<\/code> is considered <em>profane<\/em>, &#8220;some swear words&#8221; becomes &#8220;some \ud83d\udca9 words&#8221;.<\/li>\n<li>A detailed job summary is produced and the profanity filter logs the offending content to the Action logs.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>Job summaries are useful for auditing purposes as they provide a detailed account of the profane content that was replaced. This is especially useful for repositories that have strict content guidelines. Later in this post, I&#8217;ll share an example job summary.<\/p>\n<h2>Authoring .NET GitHub Actions<\/h2>\n<p>GitHub Actions are deployed as containerized apps, and they need to run quickly and efficiently. This is especially true for Actions that are run on every pull request, such as build validation.<\/p>\n<p>To publish your .NET app as a container image, use the .NET CLI <code>publish<\/code> command. The .NET CLI has built-in support for building and publishing container images without the use of a <em>Dockerfile<\/em>, and it&#8217;s as simple as running the following command:<\/p>\n<pre><code class=\"language-bash\">dotnet publish \/t:PublishContainer<\/code><\/pre>\n<p>For more information, see the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/docker\/publish-as-container\">Containerize a .NET app with <code>dotnet publish<\/code><\/a> documentation.<\/p>\n<p>Writing .NET apps that are used as GitHub Actions isn&#8217;t something that&#8217;s new, I covered this is previous posts:<\/p>\n<ul>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-loves-github-actions\">.NET \ud83d\udc9c GitHub Actions: Intro to GitHub Actions for .NET<\/a><\/li>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/automate-code-metrics-and-class-diagrams-with-github-actions\">Automate code metrics and class diagrams with GitHub Actions<\/a><\/li>\n<\/ul>\n<p>While it&#8217;s not new, we&#8217;ve come a long way since then and I have some applied learnings that I&#8217;m excited to share!<\/p>\n<h3>Optimize consuming GitHub Action workflows<\/h3>\n<p>To make your Action run more quickly, it&#8217;s best to build and publish your container image separately from the consuming workflow. One common pitfall is to define your <em>action.yml<\/em> to run using the <em>Dockerfile<\/em> as the <code>image<\/code>, rather than the container image. In other words, if you see something like the following in your <em>action.yml<\/em> file\u2014there&#8217;s likely an opportunity for improvement:<\/p>\n<pre><code class=\"language-yaml\">runs: #  \ud83d\ude48\n  using: \"docker\"\n  image: \"Dockerfile\"<\/code><\/pre>\n<p>This causes your Action to build the container image on every run, which is slow and inefficient. For example, each workflow run needs to pull down the <em>Dockerfile<\/em> to build the image before running it. Instead, you should build your container image once (or as needed) and publish it to a container registry such as Docker Hub or the GitHub Container Registry. This avoids the cost of building the container image on every run.<\/p>\n<p>Ideally, your <em>action.yml<\/em> file should point to a container image that&#8217;s already built and published. For example, if you&#8217;re using GitHub Container Registry, you can use the following syntax:<\/p>\n<pre><code class=\"language-yaml\">runs: #  \ud83e\udd13\n  using: \"docker\"\n  image: \"docker:\/\/ghcr.io\/username\/your-dotnet-action:latest\"<\/code><\/pre>\n<p>For more information on the <em>action.yml<\/em> syntax, see the <a href=\"https:\/\/docs.github.com\/actions\/creating-actions\/metadata-syntax-for-github-actions\">Metadata syntax for GitHub Actions<\/a> documentation.<\/p>\n<p>I&#8217;ve consistently observed .NET container image build times taking on average 1-2 minutes, and this is a significant amount of time for a GitHub Action to run. By building and publishing your container image separately from the consuming workflow, you can avoid this cost.<\/p>\n<h3>Publishing to the GitHub Container Registry<\/h3>\n<p>The GitHub Container Registry is a great place to publish your container images. It&#8217;s integrated with GitHub, so you can use your GitHub username and the GitHub Action&#8217;s <code>${{ secret.GITHUB_TOKEN }}<\/code> to authenticate. This is especially useful for GitHub Actions, as it&#8217;s very convenient to not have to worry about managing any additional repository secrets or variables, such as credentials for Docker Hub.<\/p>\n<p>Potty Mouth eats its own dog food. In other words, the GitHub repo that contains the source code for the Action also has a workflow that consumes the published version itself. You can see the <a href=\"https:\/\/github.com\/IEvangelist\/profanity-filter\">Potty Mouth container image<\/a> in the GitHub Container Registry, and view the GitHub Action in the <a href=\"https:\/\/github.com\/marketplace\/actions\/potty-mouth\">public marketplace<\/a>. Consider the following release workflow that publishes the app to the GitHub Container Registry:<\/p>\n<pre><code class=\"language-yml\">name: Release\n\non:\n  release:\n    types: [published]\n\nenv:\n  IMAGE_NAME: ievangelist\/profanity-filter\n  BASE_IMAGE: mcr.microsoft.com\/dotnet\/nightly\/runtime-deps:8.0-jammy-chiseled-aot\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Check out the repo\n        uses: actions\/checkout@main\n\n      - name: Get the version\n        run: echo \"RELEASE_VERSION=${GITHUB_REF\/refs\\\/tags\\\/\/}\" &gt;&gt; $GITHUB_ENV\n\n      - uses: actions\/setup-dotnet@main\n\n      - name: Login to GitHub Container Registry\n        uses: docker\/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Publish app\n        working-directory: .\/src\/ProfanityFilter.Action\n        run: |\n          dotnet publish \\\n            \/t:PublishContainer \\\n            -p DebugType=none \\\n            -p ContainerBaseImage=${{ env.BASE_IMAGE }} \\\n            -p ContainerRegistry=ghcr.io \\\n            -p ContainerImageTags='\"latest;${{ env.RELEASE_VERSION }}\"' \\\n            -p ContainerRepository=${{ env.IMAGE_NAME }} \\\n            -bl\n\n      - uses: actions\/upload-artifact@main\n        if: always()\n        with:\n          name: msbuild.binlog\n          path: src\/ProfanityFilter.Action\/msbuild.binlog<\/code><\/pre>\n<p>The preceding workflow is used to publish the Potty Mouth container image to the GitHub Container Registry. The workflow is triggered when a new release is published. The workflow uses the <code>dotnet publish<\/code> command to build and publish the container image. The <code>dotnet publish<\/code> command is configured to use the <code>ghcr.io<\/code> registry, and specifies <code>latest<\/code> and dynamic version tags. The <code>latest<\/code> tag is used to represent the latest release, and the dynamic version tag is used for the specific release version. The <code>dotnet publish<\/code> command also generates a <code>msbuild.binlog<\/code> file, which is uploaded as an artifact. This is useful for debugging purposes, as it contains detailed build information.<\/p>\n<p>The <code>BASE_IMAGE<\/code> is what the resulting container image is based on. In this case, it&#8217;s the .NET 8 nightly runtime-deps image. This specific base image is the <code>mcr.microsoft.com\/dotnet\/nightly\/runtime-deps:8.0-jammy-chiseled-aot<\/code> variant and it&#8217;s optimized for Native AOT.<\/p>\n<p>Why am I using Native AOT? Previously, when I was basing my image on <code>mcr.microsoft.com\/dotnet\/nightly\/runtime:8.0<\/code>, the resulting image was over 200 MB. Using the Native AOT base image results in a container image that&#8217;s only 38 MB. This is a significant reduction in size, as the container image is a meager 19% of the size of the original! The image also starts up much faster, which is especially useful for GitHub Actions.<\/p>\n<p>For more information on the available .NET images, see the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/docker\/container-images\">.NET container images<\/a> documentation.<\/p>\n<h3>Improving the GitHub Action developer experience for .NET<\/h3>\n<p>Our friends at GitHub maintain the official <code>@actions\/toolkit<\/code>, which enables GitHub Action authors who target JavaScript and TypeScript to have a great developer experience. Their toolkit provides a set of APIs that make interfacing with GitHub Actions feel seamless. I set out to enable .NET app developers to have a similar experience, while providing dependency injection and other common patterns and idioms found in modern .NET app development. I&#8217;ve been using my <a href=\"https:\/\/davidpine.net\/blog\/github-actions-sdk\">GitHub Actions Workflow .NET SDK<\/a> with much success despite not having full feature parity. Some of the features that I&#8217;ve implemented correspond to the existing toolkit packages, such as:<\/p>\n<table>\n<thead>\n<tr>\n<th>Original toolkit package<\/th>\n<th>.NET \ud83d\udce6 package<\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>@actions\/core<\/code><\/td>\n<td><a href=\"https:\/\/www.nuget.org\/packages\/GitHub.Actions.Core#readme-body-tab\"><code>GitHub.Actions.Core<\/code><\/a><\/td>\n<td>Provides a way to get and set environment variables, inputs, and outputs. Author job summaries, set secrets, signal failures, style outputs, and execute commands.<\/td>\n<\/tr>\n<tr>\n<td><code>@actions\/github<\/code><\/td>\n<td><a href=\"https:\/\/www.nuget.org\/packages\/GitHub.Actions.Octokit#readme-body-tab\"><code>GitHub.Actions.Octokit<\/code><\/a><\/td>\n<td>Provides a way to interact with the GitHub REST API, via <code>GitHub.Octokit.SDK<\/code> package.<\/td>\n<\/tr>\n<tr>\n<td><code>@actions\/glob<\/code><\/td>\n<td><a href=\"https:\/\/www.nuget.org\/packages\/GitHub.Actions.Glob#readme-body-tab\"><code>GitHub.Actions.Glob<\/code><\/a><\/td>\n<td>Provides a way to match file paths using glob patterns, specifically using <a href=\"https:\/\/github.com\/IEvangelist\/pathological.globbing\"><code>Pathological.Globbing<\/code><\/a>.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The project is open-source and I encourage you to check it out:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/IEvangelist\/dotnet-github-actions-sdk\">GitHub Actions Workflow .NET SDK repository<\/a><\/li>\n<\/ul>\n<h3>Develop .NET apps with Native AOT compilation<\/h3>\n<p>When I initially started rewriting the original app, I knew that I wanted to target GitHub Actions and I wanted my app to use Native AOT compilation. My friend Eric Erhardt recently blogged about <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/creating-aot-compatible-libraries\">making libraries compatible with Native AOT<\/a>. Support for Native AOT was introduced in .NET 7, and it&#8217;s a terrific way to optimize your .NET app. Native AOT compiles your .NET app to a native binary, which results in faster startup times and reduced memory usage. As mentioned previously, this is very useful when writing GitHub Actions as they need to run quickly and efficiently.<\/p>\n<p>I realized at the time of development that I couldn&#8217;t use the traditional <a href=\"https:\/\/github.com\/octokit\/octokit.net\">Octokit .NET SDK<\/a>, as it wasn&#8217;t compatible with Native AOT. This was a blocker for me, so I set out to create an issue on the Octokit repository. Like a good open source developer, I first looked for related issues and found one that was already open. I added my use case to the existing issue, and I was pleased to see that the maintainers were already working on a solution. I was able to contribute to the conversation and provide feedback on the proposed solution. This is a great example of how open source projects can benefit from community involvement. The issue details with all of my findings are detailed here:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/octokit\/octokit.net\/issues\/2737\">Octokit .NET SDK issue #2737<\/a><\/li>\n<\/ul>\n<p>The <strong>TL;DR<\/strong> is that the Octokit .NET SDK will likely not add support for Native AOT, but don&#8217;t fret! The GitHub SDK team has <a href=\"https:\/\/github.com\/orgs\/octokit\/discussions\/71\">officially annouced<\/a> that their SDKs are moving towards a generated client approach. This approach comes with many benefits, including (but not limited to):<\/p>\n<ul>\n<li>\u2705 Native AOT support.<\/li>\n<li>\ud83d\udcaf 100% REST API coverage.<\/li>\n<li>\ud83c\udf89 The ability to generate clients for .NET, CLI, Go, Java, PHP, Python, Ruby, and TypeScript.<\/li>\n<li>\u26a1 Capability to update client SDKs as frequently as the underlying API changes.<\/li>\n<\/ul>\n<p>The GitHub SDK team has been active in collaborating with the Microsoft Kiota team. Microsoft Kiota is a project that provides a single codebase for generating client SDKs for various languages. It generates clients based on a provided OpenAPI specification. The new <a href=\"https:\/\/github.com\/octokit\/dotnet-sdk\">GitHub Octokit .NET SDK<\/a> is generated using Kiota, and it will be compatible with Native AOT. This is a win-win for everyone, and I&#8217;m excited to see the new Octokit SDK in action. Consider the following example code demonstrating how the new .NET Octokit SDK can be used to interact with the GitHub REST API:<\/p>\n<pre><code class=\"language-csharp\">using GitHub;\nusing GitHub.Octokit.Client;\nusing GitHub.Octokit.Authentication;\n\nvar token = Environment.GetEnvironmentVariable(\"GITHUB_TOKEN\") ?? \"\";\nvar request = RequestAdapter.Create(new TokenAuthenticationProvider(\"Octokit.Gen\", token));\nvar gitHubClient = new GitHubClient(request);\n\n\/\/ Calls the GitHub REST HTTP:   GET \/repos\/{owner}\/{repo}\/pulls\nvar pullRequests = await gitHubClient.Repos[\"octokit\"][\"octokit.net\"].Pulls.GetAsync();\n\nforeach (var pullRequest in pullRequests)\n{\n    Console.WriteLine($\"#{pullRequest.Number} {pullRequest.Title}\");\n}<\/code><\/pre>\n<p>A few things to take note of: First, Kiota generates clients with a nested builder-approach where each collection in the corresponding REST API is a nested property. This is a great way to ensure that you&#8217;re using the API correctly. Second, my <code>GitHub.Actions.Octokit<\/code> NuGet \ud83d\udce6 package provides DI support. It also exposes the <code>GitHubClient<\/code> hydrated from the contextual <code>GITHUB_TOKEN<\/code>, which is especially useful in a GitHub Action.<\/p>\n<p>For more information, see the official <a href=\"https:\/\/learn.microsoft.com\/openapi\/kiota\">Kiota docs<\/a> and <a href=\"https:\/\/github.com\/microsoft\/kiota\">Kiota GitHub repository<\/a>.<\/p>\n<h2>Consuming the profanity filter<\/h2>\n<p>In any of your GitHub repositories, you can use the profanity filter by adding a workflow file to the <code>.github\/workflows<\/code> directory. The following example demonstrates how to use the profanity filter in a workflow:<\/p>\n<pre><code class=\"language-yml\">name: Profanity filter\n\non:\n  issue_comment:\n    types: [created, edited]\n  issues:\n    types: [opened, edited, reopened]\n  pull_request:\n    types: [opened, edited, reopened]\n\njobs:\n  filter:\n    name: Apply profanity filter\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      pull-requests: write\n\n    steps:\n    - name: Filter\n      if: ${{ github.actor != 'dependabot[bot]' &amp;&amp; github.actor != 'github-actions[bot]'  }}\n      uses: IEvangelist\/profanity-filter@main\n      id: profanity-filter\n      with:\n        token: ${{ secrets.GITHUB_TOKEN }}\n        replacement-strategy: bold-grawlix<\/code><\/pre>\n<p>The preceding GitHub Action workflow file demonstrates how to use the profanity filter. The workflow is triggered when an issue comment is created or edited, when an issue is opened, edited, or reopened, or when a pull request is opened, edited, or reopened. The workflow runs on <code>ubuntu-latest<\/code> and has the necessary permissions to write to issues and pull requests. The workflow uses the <code>IEvangelist\/profanity-filter<\/code> Action, and specifies the <code>bold-grawlix<\/code> replacement strategy. The <code>GITHUB_TOKEN<\/code> secret is used to authenticate with the GitHub REST API.<\/p>\n<p>With this example workflow in place, you can easily test the profanity filter by creating an issue or pull request with profane content. The profanity filter will detect the profane content and replace it with the specified replacement strategy. The profanity filter will also produce a detailed job summary, which is useful for auditing purposes.<\/p>\n<p>For more information, see the <a href=\"https:\/\/github.com\/IEvangelist\/profanity-filter\/blob\/main\/.github\/workflows\/dogfood.yml\">profanity filter Dogfood.yml<\/a>.<\/p>\n<h2>Scrutinizing the results<\/h2>\n<p>In the early stages, the profanity filter workflow would build the container image with every run, which was slow, inefficient, and unnecessary. However, after separating the building of the container image, execution time was reduced by 1-2 minutes. Furthermore, compiling with Native AOT resulted in a significantly smaller container image\u2014approximately one fifth the size\u2014and faster startup.<\/p>\n<p>Here&#8217;s an example run of the profanity filter:<\/p>\n<p><img decoding=\"async\" src=\".\/example-fast-run.png\" alt=\"Apply profanity filter, example run.\" \/><\/p>\n<p>In the image above, the <code>docker pull ghcr.io\/ievangelist\/profanity-filter:latest<\/code> completed in <strong>1 second<\/strong>, and the <code>docker run<\/code> of the app took 0 seconds (although it actually took 0.47 seconds). The total runtime was <strong>4 seconds<\/strong>, a significant improvement compared to previous iterations that took minutes to run. I&#8217;ve been consistently experiencing total run times of roughly 8-20 seconds from the trigger causing the workflow to be queued. While results may vary, these are huge improvements overall.<\/p>\n<h3>Explore an example issue<\/h3>\n<p>Let&#8217;s quickly explore what happens when an issue is created and it triggers the profanity filter. I created a private GitHub repo named <em>actions<\/em> where I could test out the profanity filter. I configured the workflow to include a few additional profane words (even though they&#8217;re not actually profanity this is helpful to test things, without actually writing an issue with profanity in it), the following words are manually added to the profane list:<\/p>\n<ul>\n<li><code>\"custom\"<\/code><\/li>\n<li><code>\"swear\"<\/code><\/li>\n<li><code>\"words\"<\/code><\/li>\n<\/ul>\n<p>Additionally, I configured a <a href=\"https:\/\/gist.githubusercontent.com\/IEvangelist\/355ad7852bafedb4365a896d1c545a6c\/raw\/cbd4cea1592ab5acb518d139240dd5a11f42612c\/Example.ProfaneWord.List.txt\">custom profane word list given a URL<\/a> that returns a newline-separated list of additional profane words, again not actual profanity. I also specified that I wanted to use the <code>redacted-rectangle<\/code> replacement strategy.<\/p>\n<p>I then created an issue titled <code>I love WebForms<\/code> and added the following content:<\/p>\n<blockquote>\n<p>I used to work on WinForms&#8230;but then later moved to ASP.NET WebForms, and while I learned a lot, I would have preferred a purer web approach.<\/p>\n<p>Did I mention that I defined some custom swear words that will be filtered too?<\/p>\n<\/blockquote>\n<p>The profanity filter successfully detected the configured words and replaced them with the desired replacement strategy. Consider the following screen capture depicting the updated issue:<\/p>\n<p><img decoding=\"async\" src=\".\/example-issue.png\" alt=\"Example issue after being updated.\" \/><\/p>\n<p>The job summary provided a detailed account of the profane content that was replaced, as shown in the following screen capture:<\/p>\n<p><img decoding=\"async\" src=\".\/example-job-summary.png\" alt=\"Example issue workflow job summary.\" \/><\/p>\n<h2>Conclusion<\/h2>\n<p>The pursuit of .NET libraries compatible with Native AOT has just begun, promising a challenging yet rewarding journey for library authors. As the new Octokit .NET SDK takes center stage, I eagerly anticipate witnessing its capabilities in action and utilizing it extensively within GitHub Actions. Additionally, I am keen on tracking the progress of the innovative Kiota SDK, recognizing its immense potential.<\/p>\n<p>Initially, the Potty Mouth profanity filter workflow suffered from inefficiency due to building the container image with every run. However, by separating the image building process, we successfully eliminated 1-2 minutes of unnecessary execution time. Furthermore, leveraging Native AOT compilation resulted in a significantly smaller container image, reducing its size to nearly one fifth and enhancing startup speed. All of this collectively yielded a substantial improvement in the overall execution time of the profanity filter workflow.<\/p>\n<p><!-- Named links --><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Explore a fun example app targeting GitHub Actions written entirely in .NET, optimized with Native AOT, and published to the GitHub Container Registry.<\/p>\n","protected":false},"author":24662,"featured_media":50731,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7589],"tags":[7789,57,7608,7799,7801,92,7798,7800,7802],"class_list":["post-50524","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-github-actions","tag-net-cli","tag-containers","tag-github-actions","tag-github-container-registry","tag-kiota","tag-linux","tag-native-aot","tag-octokit","tag-openapi"],"acf":[],"blog_post_summary":"<p>Explore a fun example app targeting GitHub Actions written entirely in .NET, optimized with Native AOT, and published to the GitHub Container Registry.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/50524","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/24662"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=50524"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/50524\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/50731"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=50524"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=50524"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=50524"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}