Packaging with GitHub Actions
Learn to automate the packaging process by integrating CPack with a multi-platform GitHub Actions workflow
In the last lesson, we learned how to use CPack to bundle our project's install-tree into a distributable package like a .zip
archive. We can now run cpack --preset dev-zip
on our local machine to generate a package.
Our CI pipeline already automates building and testing. The logical next step is to integrate our packaging step into this automated workflow. By the end of this lesson, our GitHub Actions workflow will not only verify our code but also automatically produce distributable packages for Windows, macOS, and Linux, making them available for download.
Automating Package Creation
This lesson builds on the GitHub Actions workflow we built . Our workflow checks out our code, sets up vcpkg, and then uses our CMake presets to configure, build, and run our tests on a single operating system:
.github/workflows/ci.yml
name: C++ CI
on:
pull_request:
branches: [ "main" ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up vcpkg
uses: lukka/run-vcpkg@v11
- name: Configure CMake
run: cmake --preset default
- name: Build project
run: cmake --build --preset default
- name: Run tests
run: ctest --preset default
Our goal is to add two new capabilities to this workflow:
- Run CPack after the build to create our package.
- Save the generated package file so we can download it after the CI run is complete.
Adding a CPack Step to the Workflow
This is the easiest step. Since our build already has this capability and CMake presets to support it, all we need to do is add a new run
step that executes cpack
using our preset.
For now, we'll also update our configure, build, and test steps to use the release
presets, but we'll introduce a way to have our workflow run multiple configurations using a matrix strategy soon.
For packaging, we'll use the dev-zip
preset we created in the :
.github/workflows/ci.yml
name: C++ CI
on:
pull_request:
branches: ["main"]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up vcpkg
uses: lukka/run-vcpkg@v11
- name: Configure CMake
run: cmake --preset release
- name: Build project
run: cmake --build --preset release
- name: Run tests
run: ctest --preset release
- name: Package
run: cpack --preset dev-zip
When this step runs, CPack will execute and create the package file inside the build directory on the CI runner. If we look at the logs for this step in GitHub Actions, we should see the familiar output from CPack:

This is a great start, but we have an immediate problem: how do we download this package? Where is it even stored?
Saving the Package with Workflow Artifacts
Any files created on a CI runner are temporary. Once the job finishes, the virtual machine is destroyed, and all its files are lost. To preserve the output of a job, we need to use workflow artifacts.
An artifact is a file or a collection of files that are saved from a workflow run. Once a job is complete, you can view and download its artifacts from the workflow's summary page on GitHub.
Using the upload-artifact
Action
The standard way to create an artifact is with the official actions/upload-artifact
action. Let's add this as the final step in our job.
.github/workflows/ci.yml
name: C++ CI
on:
pull_request:
branches: ["main"]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up vcpkg
uses: lukka/run-vcpkg@v11
- name: Configure CMake
run: cmake --preset release
- name: Build project
run: cmake --build --preset release
- name: Run tests
run: ctest --preset release
- name: Package
run: cpack --preset dev-zip
- name: Upload Package Artifact
uses: actions/upload-artifact@v4
with:
name: Greeter-Package-Linux
path: build/Greeter-*-Development.zip
Let's break down the with
block, which contains the inputs for this action:
name
: This is the name the artifact will have on the GitHub UI. It's a good idea to make this descriptive, including the platform.path
: This tells the action which file(s) to upload. CPack generates filenames that include the version and architecture (e.g.,Greeter-1.0.0-Linux.zip
). Since this can change, we use a glob pattern (*
) to match any file that starts withGreeter-
and ends with-Development.zip
in thebuild/
directory.
Now, the actions/upload-artifact
will provide a link to download our package within its log output. Also, after our workflow completes, if we go to the "Summary" page for the run, we'll see a new "Artifacts" section with our package, ready to be downloaded:

Cross-Platform Packages using a Matrix Strategy
GitHub Actions has a useful feature called a strategy: matrix
. It allows us to define a set of configurations, and GitHub will automatically create a separate job for each combination. As an example, let's use this to create builds and packages for three platforms - Linux, Windows, and macOS.
Let's refactor our ci.yml
to use a matrix. We'll define a list of operating systems and let GitHub Actions run our build-and-test
job in parallel on each one:
.github/workflows/ci.yml
name: C++ CI
on:
pull_request:
branches: ["main"]
jobs:
build-and-test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up vcpkg
uses: lukka/run-vcpkg@v11
- name: Configure CMake
run: cmake --preset release
- name: Build project
run: cmake --build --preset release
- name: Run tests
run: ctest --preset release
- name: Package
run: cpack --preset dev-zip
- name: Upload Package Artifact
uses: actions/upload-artifact@v4
with:
name: Greeter-Package-${{ matrix.os }}
path: build/Greeter-*-Development.zip
Let's examine the changes:
The strategy: matrix: os: [...]
setting define a matrix with a single variable, os
, with three values representing the list of runner images we want to use.
Then, instead of hardcoding a runner for the runs-on
option, we use the special context syntax ${{ ... }}
to access the os
variable from the matrix.
We also use this variable in the name
we pass to upload-artifact
. This ensures each of our packages have a unique name, which will help us distinguish between them.
Now, when this workflow runs, GitHub will create three parallel jobs. The first job will run with matrix.os
set to ubuntu-latest
, the second with windows-latest
, and the third with macos-latest
.
The result is a parallel, multi-platform build from a single job definition:

This is why keeping our build scripts portable is important. If they're portable, everything will just work.
Once all of our builds are complete, our workflow summary page will show three downloadable packages - one from each of our parallel jobs:

More Examples
This example use of a matrix created an os
variable, but we can use this technique to solve a wide range of problems. For example, we could create a matrix strategy to handle different build configurations, implemented by controlling which presets are used:
name: C++ CI
on:
pull_request:
branches: ["main"]
jobs:
build-and-test:
strategy:
matrix:
config: [debug, release]
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up vcpkg
uses: lukka/run-vcpkg@v11
- name: Configure CMake
run: cmake --preset ${{ matrix.config }}
- name: Build project
run: cmake --build --preset ${{ matrix.config }}
- name: Run tests
run: ctest --preset ${{ matrix.config }}
- name: Package
run: cpack --preset ${{ matrix.config }}-dev-zip
- name: Upload Package Artifact
uses: actions/upload-artifact@v4
with:
name: Greeter-Package-ubuntu-latest
path: build/Greeter-*-Development.zip
Our matrix strategy can also have multiple dimensions. This 3x2 matrix would spawn six jobs - one for every combination of os
and config
:
name: C++ CI
on:
pull_request:
branches: ["main"]
jobs:
build-and-test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
config: [debug, release]
runs-on: ${{ matrix.os }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up vcpkg
uses: lukka/run-vcpkg@v11
- name: Configure CMake
run: cmake --preset ${{ matrix.config }}
- name: Build project
run: cmake --build --preset ${{ matrix.config }}
- name: Run tests
run: ctest --preset ${{ matrix.config }}
- name: Package
run: cpack --preset ${{ matrix.config }}-dev-zip
- name: Upload Package Artifact
uses: actions/upload-artifact@v4
with:
name: Greeter-Package-${{ matrix.os }}-${{ matrix.config}}
path: build/Greeter-*-Development.zip
Summary
In this lesson, we've integrated our CPack configuration directly into our GitHub Actions workflow, automating the packaging process.
- Automated Packaging: We added a
run
step to our CI workflow to executecpack
after a successful build. - Workflow Artifacts: We used the
actions/upload-artifact
action to save the packages generated by CPack, making them downloadable from the workflow summary page. - Multi-Platform Builds with Matrix: We refactored our workflow to use a
strategy: matrix
. This allowed us to build and package for Windows, macOS, and Linux in parallel without duplicating our workflow code. - Using Matrix Variables: We used the
${{ matrix.os }}
context variable to give each platform's artifact a unique name.
Creating Installers with CPack and GitHub Actions
Learn to automate the creation of a professional Windows installer by integrating CPack, the WiX Toolset, and a multi-platform GitHub Actions workflow.