Skip to content

Instantly share code, notes, and snippets.

@LegalizeAdulthood
Last active April 29, 2025 15:54
Show Gist options
  • Save LegalizeAdulthood/4e839bf183164ed56ea02aa4834d77e1 to your computer and use it in GitHub Desktop.
Save LegalizeAdulthood/4e839bf183164ed56ea02aa4834d77e1 to your computer and use it in GitHub Desktop.
Using NuGet Package Caching with VcPkg and GitHub Actions

Using NuGet Package Caching with VcPkg and GitHub Actions

The GitHub Actions Cache mechanism has been removed from vcpkg, so you can't use x-gha in VCPKG_BINARY_SOURCES as a way to re-use built products from one build to the next anymore. I was using x-gha because the setup was trivial for all platforms: just set a couple environment variables.

You can use NuGet packages to achieve the same purpose, although the setup is a little more complicated. Here's what I learned while setting this up for the first time for use with GitHub Actions.

1. GitHub Classic Personal Access Token

In order for NuGet to interact with the packages associated with your github account, you will need a way to authenticate NuGet with GitHub. Do this by creating a classic public access token as described in the GitHub documentation with token name GH_PACKAGES_TOKEN. The actual name isn't important, so long as you use the same name in your workflow and repository. The token needs packages:write permission in order to cache dependencies.

When you create the token, you will only be able to see it's value once at creation time. Keep the token value in a secure location and treat it like a password. After the token is created you will not be able to view it's value again, you will only be able to create a new value.

2. Configure Repositories

Each repository that is going to use NuGet packages for caching needs to be configued with a secret to provide the access token to workflows. See Using secrets in GitHub Actions in the GitHub documentation. Configure a secret named GH_PACKAGES_TOKEN and paste in the value of the access token you generated.

3. Prerequisites

NuGet is a .NET application, so you need to be able to run .NET applications on the github runner. Even though the runner image documentation says that mono is installed by default on ubuntulatest, it gave me 'command not found' when I attempted to run mono.

On windows-latest, nuget was already installed via Chocolatey.

On ubuntu-latest, nuget was not installed. Vcpkg can fetch nuget with the command vcpkg fetch nuget. I use vcpkg as a submodule, so on initial checkout the vcpkg executable isn't available yet. It gets downloaded during a bootstrap phase in my CMake build process. However, we need to invoke nuget first to configure package locations before we invoke vcpkg via cmake. On ubuntu-latest we can run the bootstrap script to get vcpkg downloaded, so our prerequisite workflow step is as follows:

    - name: Install Prerequisites Linux
      if: ${{matrix.os == 'ubuntu-latest'}}
      shell: bash
      run: |
        ${{github.workspace}}/vcpkg/bootstrap-vcpkg.sh
        sudo apt install mono-complete

I'm using ${{github.workspace}}/vcpkg to locate the bootstrap shell script as I've got vcpkg checked out as a submodule in the root of my project.

4. Configuring NuGet Package Sources

Unlike GitHub Actions Cache, which shows cached items in the Actions tab for your repository, NuGet packages are shown in the Packages tab for your account. To configure NuGet we need to add a package source and then configure the api key for that source. This is done by invoking nuget twice:

    env: 
      FEED_URL: https://nuget.pkg.github.com/${{github.repository_owner}}/index.json

    # ...
    steps:
    - name: Add NuGet sources Linux
      if: ${{matrix.os == 'ubuntu-latest'}}
      shell: bash
      run: |
        mono `${{github.workspace}}/vcpkg/vcpkg fetch nuget | tail -n 1` \
          sources add \
          -Source "${{env.FEED_URL}}" \
          -StorePasswordInClearText \
          -Name GitHubPackages \
          -UserName "${{github.repository_owner}}" \
          -Password "${{secrets.GH_PACKAGES_TOKEN}}"
        mono `./vcpkg/vcpkg fetch nuget | tail -n 1` \
          setapikey "${{secrets.GH_PACKAGES_TOKEN}}" \
          -Source "${{env.FEED_URL}}"

    - name: Add NuGet sources Windows
      if: ${{matrix.os == 'ubuntu-latest'}}
      shell: bash
      run: |
        nuget \
          sources add \
          -Source "${{env.FEED_URL}}" \
          -StorePasswordInClearText \
          -Name GitHubPackages \
          -UserName "${{github.repository_owner}}" \
          -Password "${{secrets.GH_PACKAGES_TOKEN}}"
        nuget \
          setapikey "${{secrets.GH_PACKAGES_TOKEN}}" \
          -Source "${{env.FEED_URL}}"

When we use vcpkg to fetch nuget, the last line of the output from vcpkg is the local path where nuget was stored; hence the unusual shell trickery to invoke nuget:

mono `vcpkg fetch nuget | tail -1`

I'm using ${{github.repository_owner}} to identify the user associated with the package cache to NuGet and the GH_PACKAGES_TOKEN secret mentioned above.

4.1. Squishing Out Duplication

The above steps are identical, except for how they invoke nuget. (Did you realize you could run steps on the windows-latest runner using the bash shell? I didn't until I worked this out.) We can factor out this duplication by using a matrix strategy.

    strategy:
      matrix:
        os: [ ubuntu-latest, windows-latest ]
        # Disabled until <curses.h> can be safely included without conflict in terminal.h
        # os: [ ubuntu-latest, windows-latest, macos-latest ]
        include:
          - os: ubuntu-latest
            nuget: mono `./vcpkg/vcpkg fetch nuget | tail -n 1`
          - os: windows-latest
            nuget: nuget

    runs-on: ${{matrix.os}}
    env: 
      FEED_URL: https://nuget.pkg.github.com/${{github.repository_owner}}/index.json

    steps:
    # ...
    
    - name: Add NuGet sources
      shell: bash
      run: |
        ${{matrix.nuget}} \
          sources add \
          -Source "${{env.FEED_URL}}" \
          -StorePasswordInClearText \
          -Name GitHubPackages \
          -UserName "${{github.repository_owner}}" \
          -Password "${{secrets.GH_PACKAGES_TOKEN}}"
        ${{matrix.nuget}} \
          setapikey "${{secrets.GH_PACKAGES_TOKEN}}" \
          -Source "${{env.FEED_URL}}"

In the build matrix, I've set different values for the nuget parameter to the command used to invoke nuget in the two environments. Note that I replaced ${{github.workspace}} with . for ubuntu because the matrix parameters are created before I've done a checkout step and the checkout step is what sets github.workspace. This is potentially fragile, but the steps are always run in the checkout directory, so I don't think this is really a problem.

5. Configure VcPkg Binary Cache Sources

Once NuGet has been configured and authenticated with GitHub, we can tell vcpkg to use the nuget binary caching method:

    - name: CMake workflow
      env: 
        VCPKG_BINARY_SOURCES: "clear;nuget,${{env.FEED_URL}},readwrite"
      run: cmake --workflow --preset default

Setting the VCPKG_BINARY_SOURCES environment variable configures vcpkg to use the NuGet feed that is authenticated against our github account.

If everything has been configured correctly, your workflow runs should start publishing private packages to your github account that are reused between workflow runs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment