Help us improve Softanics
We use analytics cookies to understand which pages and downloads are useful. No ads. Privacy Policy
Artem Razin
Low-level software protection engineer with 20+ years in native and managed code security. Creator of ArmDot, protecting commercial .NET applications since 2014.

.NET Obfuscation in GitHub Actions

If ArmDot is already integrated into your project via MSBuild and NuGet, adding obfuscation to a GitHub Actions workflow requires no additional tooling and no extra pipeline steps. The workflow runs dotnet build or dotnet publish, and the MSBuild target handles obfuscation automatically. The published artifact comes out protected.

If you have not yet added the NuGet packages and MSBuild target to your project, do that first: Integrating .NET Obfuscation with MSBuild and NuGet →

The workflow does not change

A standard .NET release workflow in GitHub Actions already produces obfuscated output if the MSBuild target is in the .csproj. No obfuscation-specific steps are needed:

on:
  push:
    branches: [main]
 
jobs:
  build:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'
      - name: Restore dependencies
        run: dotnet restore
      - name: Publish
        run: dotnet publish --configuration Release --no-restore --output ./publish
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: protected-build
          path: ./publish

The dotnet publish step triggers the MSBuild target, which runs ArmDot on the compiled assembly in the intermediate output directory. The uploaded artifact is already obfuscated.

In demo mode (no license key provided), ArmDot is fully functional but protected assemblies stop working after two weeks. For production builds, the license key needs to be passed through the workflow.

Passing the license key on Linux runners

GitHub Actions stores secrets as encrypted values exposed to workflows as environment variables. The license key needs to be written to a file, and the file path passed to the MSBuild task via ARMDOT_LICENSE_FILE_PATH.

on:
  push:
    branches: [main]
 
jobs:
  build:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'
      - name: Restore dependencies
        run: dotnet restore
      - name: Save license
        run: |
          echo $ARMDOT_LICENSE_KEY >> ${{ runner.temp }}/ArmDotLicenseKey
        shell: bash
        env:
          ARMDOT_LICENSE_KEY: ${{ secrets.ARMDOT_LICENSE_KEY }}
      - name: Publish
        run: dotnet publish --configuration Release --no-restore --output ./publish
        env:
          ARMDOT_LICENSE_FILE_PATH: ${{ runner.temp }}/ArmDotLicenseKey
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: protected-build
          path: ./publish

The "Save license" step writes the secret to a temporary file. The "Publish" step passes the file path to the MSBuild task through the environment variable. The MSBuild target reads it via LicenseFile="$(ARMDOT_LICENSE_FILE_PATH)" in the .csproj.

To set up the secret: navigate to your repository on GitHub, go to Settings > Secrets and variables > Actions, click New repository secret, name it ARMDOT_LICENSE_KEY, and paste the license key content. The secret is encrypted at rest and does not appear in build logs.

Passing the license key on Windows runners

Windows runners use cmd as the default shell, which changes the syntax. One important constraint: the license key must be stored as a single line in the GitHub secret, because cmd does not handle multi-line values correctly in this context.

      - name: Save license
        run: |
          echo "%ARMDOT_LICENSE_KEY%" >> ${{ runner.temp }}\ArmDotLicenseKey
        shell: cmd
        env:
          ARMDOT_LICENSE_KEY: ${{ secrets.ARMDOT_LICENSE_KEY }}
      - name: Publish
        run: dotnet publish --configuration Release --no-restore --output ./publish
        env:
          ARMDOT_LICENSE_FILE_PATH: ${{ runner.temp }}/ArmDotLicenseKey

The differences from the Linux workflow: shell: cmd, backslash in the temp file path for the echo command, and the %ARMDOT_LICENSE_KEY% syntax instead of $ARMDOT_LICENSE_KEY.

Choosing the right runner

For most .NET projects, ubuntu-latest is the right choice. ArmDot runs on Linux, Windows, and macOS with no platform-specific installation - the NuGet packages are restored by dotnet restore like any other dependency. Linux runners are faster to provision and cost less on GitHub's billing.

Use windows-latest if your build depends on Windows-specific tooling - COM/ActiveX interop, native Windows SDKs, or other dependencies that are not available on Linux. The obfuscation behavior is identical on both platforms - the runner choice is determined by build requirements, not by ArmDot.

Debug builds vs release builds

Obfuscation runs on every build by default, including local debug builds. For developers who want unobfuscated debug builds for easier local debugging while keeping release builds protected, add a condition to the MSBuild target:

<Target Name="Protect" AfterTargets="AfterCompile" BeforeTargets="BeforePublish"
        Condition="'$(Configuration)' == 'Release'">

With this condition, dotnet build in Debug configuration skips obfuscation entirely, and dotnet publish --configuration Release in the workflow produces the protected output. This is the most common setup for teams that want to debug locally against unobfuscated code while shipping obfuscated releases.

Verifying the artifact is obfuscated

A successful obfuscation run shows [ArmDot] lines in the build log:

[ArmDot] Names obfuscation started
[ArmDot] Names obfuscation finished
[ArmDot] Conversion started for method ...
[ArmDot] Writing protected assembly to ...

If you see warning ARMDOT0003: No methods to protect, the MSBuild target is running but no protection attributes have been applied in the source code. If the [ArmDot] lines are absent entirely, the MSBuild target is not in the .csproj - return to the MSBuild/NuGet integration guide.

Build logs confirm the process ran. To confirm the output is actually protected, download the published artifact and open the main assembly in ILSpy. Non-public members should show obfuscated names. Virtualized methods should show the VM interpreter loop rather than the original logic. String literals should be absent from the decompiled output.

As a quick sanity check without ILSpy: compare the file size of the obfuscated assembly to the unobfuscated version. Virtualized assemblies are noticeably larger because the embedded VM and encoded bytecode buffers add to the binary size. A release build that is the same size as a debug build is a signal that obfuscation may not have run.

For a complete step-by-step walkthrough starting from a new project, including screenshots of the GitHub UI: How to obfuscate .NET application in GitHub workflow →

Back to: .NET Obfuscation in CI/CD →

ArmDot in GitHub Actions

ArmDot runs on Linux, Windows, and macOS GitHub Actions runners with no platform-specific installation. The NuGet packages are restored by dotnet restore like any other dependency. Full documentation →