Build and Publish a ROS Snap with GitHub Actions
When deploying a robotics application with snap, keeping the deployment synchronised with development progress is a high priority. It’s best accomplished with a CI/CD pipeline that will automatically deploy your latest developments to your devices.
In this post, we will explore how to automatically build and publish your ROS snap using GitHub Actions, so that your snap always stays up-to-date on the snapstore.
For this example, we will use a simple ROS2 Foxy application publishing some dummy data, whose source code can be found on GitHub . Of course, the same principle can be applied to a ROS package or another release of ROS 2.
Adding a Snap GitHub Action
Our example project is called snapped_ros2_pkg and it has a typical ROS 2 package structure:
.
├── CMakeLists.txt # Compilation instructions
├── config
│ └── config.yaml # Configuration file
├── launch
│ └── snapped.launch.py # Python ROS 2 launchfile
├── package.xml # Dependencies list
├── README.md
├── snap
│ └── snapcraft.yaml # Snapcraft entry point
└── src
└── snapped_ros2_pkg_node.cpp # Source of the node publishing the dummy data
GitHub’s workflows are located in .github/workflows/ in our project tree:
├── .github
└── workflows
└── snap.yaml
The file snap.yaml
will contain our GitHub workflow dedicated to snap. The GitHub workflow will first build the snap before testing it publishing it to the snap store.
Here is the content of the snap.yaml
. Don’t worry, we’ll break this down:
name: snap
on:
push:
tags:
- '*'
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
outputs:
snap-file: ${{ steps.build-snap.outputs.snap }}
steps:
- uses: actions/checkout@v3
- uses: snapcore/action-build@v1
id: build-snap
# Make sure the snap is installable
- run: |
sudo snap install --dangerous ${{ steps.build-snap.outputs.snap }}
# Do some testing with the snap
- run: |
gbeuzeboc-snapped-ros2-pkg.snapped-ros2-launch --print-description
- uses: actions/upload-artifact@v3
with:
name: gbeuzeboc-snapped-ros2-pkg
path: ${{ steps.build-snap.outputs.snap }}
publish:
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@v3
with:
name: gbeuzeboc-snapped-ros2-pkg
path: .
- uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
with:
snap: ${{needs.build.outputs.snap-file}}
release: ${{ startsWith(github.ref, 'refs/tags/') && 'candidate' || 'edge'}}
This workflow is already in use, and you can see the results here .
Workflow conditions
A GitHub workflow file starts with its name and the run conditions.
In our case, we will only trigger the workflow on changes on the branch main
, a new tag, as well as when a pull request is opened against main
. We additionally added a condition (workflow_dispatch
) to be able to trigger it manually.
name: snap
on: # The GitHub Action will happen when:
push:
tags:
- '*' # When a new tag is created
branches:
- main # When a commit was created on main
pull_request:
branches:
- main # When a pull request is created on main
Workflow_dispatch: # When we trigger it manually via the API or the web interface
For more information about triggering a GitHub workflow, see, ”on” documentation.
Workflow jobs
In our workflow, we will define two distinct jobs. One for building the snap and another for publishing the snap. The idea is that we want to build the snap every time we run the action, but only publish under certain conditions.
Build job
Before listing the steps of our build, we will define the environment and the output of our build. The output will be used to transfer the result of the build job to the publish
job. This is particularly helpful since you can download the artifact - the snap - from the build webpage.
build:
runs-on: ubuntu-latest
outputs:
snap-file: ${{ steps.build-snap.outputs.snap }}
Here we use the variable steps.build-snap.outputs.snap
that will be defined by a step right after.
For more information about the type of machine to run the job on and outputs, see, runs-on
and outputs
.
Checkout step
GitHub Action comes with reusable actions for CI/CD provided by GitHub and the community! We are going to use some common ones as well as some that are specific to snap.
The first step is to checkout our current code using the checkout workflow.
- uses: actions/checkout@v2
Build the snap
In order to build our snap, we will use the snapcore/action-build action.
- uses: snapcore/action-build@v1
id: build-snap
This will generate our .snap
file and store its name in the variable steps.build-snap.outputs.snap
.
Test our snap
Now we should make sure that our snap is installable and can run.
# Make sure the snap is installable
- run: |
sudo snap install --dangerous ${{ steps.build-snap.outputs.snap }}
# Do some testing with the snap
- run: |
gbeuzeboc-snapped-ros2-pkg.snapped-ros2-launch --print-description
Here we are reusing the variable steps.build-snap.outputs.snap
to get our snap name and feed it to the snap install command. Then we simply run the launchfile
accompanying our ROS 2 application with the --print-description
option, which only prints the launch description to the console. Which gives us a good indication that the call to our application is fine. Of course, more extensive testing could and should be implemented.
Upload our artifact
Now that our snap is generated and tested, we will upload it as an artifact. This is useful, so that we can download it from the GitHub webpage to test it locally. We will also make use of it for the second part of our workflow: publish.
- uses: actions/upload-artifact@v3
with:
name: gbeuzeboc-snapped-ros2-pkg
path: ${{ steps.build-snap.outputs.snap }}
We are now using the actions/upload-artifact . Note that before the steps, we defined snap-file: ${{ steps.build-snap.outputs.snap }}
in the output section. Later, we will refer to our uploaded artifact (our snap) as snap-file
.
Publish job
This job will be executed only under certain conditions. We only want to publish our snap when there are changes on the main
branch or when we create a new tag. Furthermore, this job can only run when the ‘build’ job is successfully done.
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') # main branch or tagged version
runs-on: ubuntu-latest
needs: build # Wait for build job to be done
By adding the build
job in the needs, this job can only happen if the build
job succeed. We will also be able to refer to the uploaded artifact of the build
job.
Download the artifact
First, we need to get the previously uploaded artifact (our .snap
file) with the actions/download-artifact action.
- uses: actions/download-artifact@v3
with:
name: gbeuzeboc-snapped-ros2-pkg
path: .
We explicitly specify path: .
so that our artifact is stored at the root of our directory, without any extra directory.
Publish the snap
Finally, we are publishing our snap with the snapcore/action-publish action. You will have to identify to the snapstore in order to publish your snap. Here we refer to the secret STORE_LOGIN
(set in the next section). The snap to upload is referred to by the output of the build job.
Additionally, we want to release to different risk levels depending on the situation. If it’s a change on the main
branch we would like to publish to the edge risk level, so edge always has the latest changes which may not be considered as stable. On the other hand, if we are on a tagged version, we would like to publish to the candidate
risk level. After thorough testing, the maintainer can manually promote the snap to beta/stable
.
- uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
with:
snap: ${{needs.build.outputs.snap-file}}
release: ${{ startsWith(github.ref, 'refs/tags/') && 'candidate' || 'edge'}}
Setting the secret
The action-publish
is using the secret STORE_LOGIN
so let us define it.
Generate the secret
Open your terminal and enter:
snapcraft export-login --snaps=gbeuzeboc-snapped-ros2-pkg --acls package_access,package_push,package_update,package_release exported.txt
Make sure to adjust the --snaps=
to your snap name.
This will prompt you to login and will then generate an exported.txt
file. The content of this file is secret.
Add the secret to your GitHub repository
To add the secret to the project, we will need to:
- Go to the “Settings” tab of the repository
- Choose “Secrets” from the menu on the left and then “Actions”
- Click on “New repository secret”.
- Setting the name of the secret as
STORE_LOGIN
, and paste the contents ofexported.txt
as the value.
We are all good to go now. The actions can be checked on the Actions tab of the project. And the published snap can be seen on the snapstore page.
You can of course create a more complex release behaviour, making use of all the channels features of the snapstore.
A real world example
While the project presented was just an example, one can find real world use of a ROS snap CI/CD workflow. A great example is the ROS snap examples repository. The ROS snap examples repository hosts mulitple examples and CIs. With the snap GitHub workflow updating the available snap, users can stay up to date on the latest version of the talker-listener.
Summary
We have explored how to build and publish a snap with GitHub Actions. This allows you to easily keep your deployment in sync with your development. With channels, providing simultaneously different versions of your software is made easy. Distributing your application with snap allows you to easily benefit from a production-grade infrastructure while offering a seamless experience to your users.