published: 4th of January 2023
For the last few months, I have been working on a static site generator named shazam. It is written in Rust and can be compiled to binaries suitable for Linux, with Windows and MacOS coming soon.
I am all about the DevOps, so of course I wanted to use Github Actions to automagically build and release the binary assets as well as publish a container image to Docker Hub.
In this post, I will show you how I use Github actions to publish binaries for Shazam to Github releases and container images to Docker hub.
Github actions use workflows to define continuous integration tasks. For this project, I have defined 3 workflows.
The following flowchart shows the Github actions workflow.
The following points summarize the diagram.
Workflow files are stored in the .github/workflows/ directory in the root of a project.
The Test Workflow runs the tests, checks formatting and code style correctness with clippy. The workflow runs whenever a pull request is created.
name: "Test"
on:
pull_request:
jobs:
check:
name: "Cargo check"
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: actions/checkout@v3
- uses: "actions-rs/toolchain@v1"
with:
profile: "minimal"
toolchain: "stable"
override: true
- uses: "actions-rs/cargo@v1"
with:
command: "check"
test:
name: "Cargo test"
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: actions/checkout@v3
- uses: "actions-rs/toolchain@v1"
with:
profile: "minimal"
toolchain: "stable"
override: true
- uses: "actions-rs/cargo@v1"
with:
command: "test"
fmt:
name: "Cargo format"
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: actions/checkout@v3
- uses: "actions-rs/toolchain@v1"
with:
profile: "minimal"
toolchain: "stable"
override: true
- run: "rustup component add rustfmt"
- uses: "actions-rs/cargo@v1"
with:
command: "fmt"
args: "--all -- --check"
clippy:
name: "Cargo clippy"
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: actions/checkout@v3
- uses: "actions-rs/toolchain@v1"
with:
profile: "minimal"
toolchain: "stable"
override: true
- run: "rustup component add clippy"
- uses: "actions-rs/cargo@v1"
with:
command: "clippy"
args: "-- -D warnings"
The Tag Workflow runs when a pull request is merged into the main branch. A tag is generated based on the project version from the Cargo.toml file and pushed to the repo.
name: "Tag"
on:
push:
branches:
- "main"
jobs:
create-tag:
name: "Create tag"
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: actions/checkout@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: "Get tag"
id: "get-tag"
shell: "bash"
run: |
echo PKG_VERSION=$(awk -F ' = ' '$1 ~ /version/ { gsub(/["]/, "", $2); printf("%s",$2) }' Cargo.toml) >> $GITHUB_OUTPUT
- name: "Set Tag"
shell: "bash"
run: |
git tag v${{ steps.get-tag.outputs.PKG_VERSION }} && git push --tags
The Release Workflow is where the magic happens. Binary assets are generated and uploaded to the projects Github as releases. A Docker container image is generated and uploaded as a tag.
To prevent infinite loops, workflows only trigger other workflows on limited events. One of these events is the workflow_run event.
The Release Workflow runs when the Tag Worflow is completed. Additionally, the steps require that the Tag Workflow completed successfuly. This is defined with an if condition: if: ${{ github.event.workflow_run.conclusion == 'success' }}.
The other thing to note, is that some steps depend on others. By default, tasks run in parallel, you can control the order tasks are run by declaring the needs parameter. EG: needs: "get-tag". In the below workflow, this creates a dependency for the create-release task, which will wait for the get-tag task to complete before executing.
name: "Release"
permissions:
contents: "write"
on:
workflow_run:
workflows: ["Tag"]
types:
- "completed"
jobs:
get-tag:
name: "Get Tag From Package Version"
runs-on: "ubuntu-latest"
outputs:
pkg-version: ${{ steps.pkg-version.outputs.PKG_VERSION }}
steps:
- name: "Check out the repo"
uses: actions/checkout@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: "Get tag"
id: "pkg-version"
shell: "bash"
run: |
echo PKG_VERSION=$(awk -F ' = ' '$1 ~ /version/ { gsub(/["]/, "", $2); printf("%s",$2) }' Cargo.toml) >> $GITHUB_OUTPUT
create-release:
name: "Create release"
if: ${{ github.event.workflow_run.conclusion == 'success' }}
needs: "get-tag"
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: actions/checkout@v3
- name: "Create release"
uses: "taiki-e/create-gh-release-action@v1"
with:
# (optional) Path to changelog.
# changelog: CHANGELOG.md
branch: "main"
ref: refs/tags/v${{ needs.get-tag.outputs.pkg-version }}
token: ${{ secrets.GITHUB_TOKEN }}
upload-assets:
name: "Upload assets to Github releases"
if: ${{ github.event.workflow_run.conclusion == 'success' }}
needs:
- "get-tag"
- "create-release"
strategy:
matrix:
include:
- target: "x86_64-unknown-linux-gnu"
os: "ubuntu-latest"
- target: "x86_64-unknown-linux-musl"
os: "ubuntu-latest"
runs-on: ${{ matrix.os }}
steps:
- name: "Check out the repo"
uses: actions/checkout@v3
- name: "Upload Binaries"
uses: "taiki-e/upload-rust-binary-action@v1"
with:
bin: "shazam"
target: ${{ matrix.target }}
archive: $bin-${{ matrix.target }}
ref: refs/tags/v${{ needs.get-tag.outputs.pkg-version }}
token: ${{ secrets.GITHUB_TOKEN }}
push-to-registry:
name: "Push Docker image to Docker Hub"
if: ${{ github.event.workflow_run.conclusion == 'success' }}
needs:
- "get-tag"
- "upload-assets"
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: actions/checkout@v3
- name: "Log in to Docker Hub"
uses: "docker/login-action@v2"
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: "Extract metadata (tags, labels) for Docker"
id: "meta"
uses: "docker/metadata-action@v4"
with:
images: "bwks/shazam"
- name: "Build and push Docker image"
uses: "docker/build-push-action@v3"
with:
context: .
push: true
tags: bwks/shazam:latest,bwks/shazam:v${{ needs.get-tag.outputs.pkg-version }}
labels: ${{ steps.meta.outputs.labels }}
My friends, we have come to the end of the road. In this post, I showed you how to create Github workflows to generate binary assets, and also generate a Docker container image for a Rust project. These assets are then uploaded as Github releases and Docker hub tags.
https://kerkour.com/rust-github-actions-ci-cd
https://mateuscosta.me/rust-releases-with-github-actions
https://alican.codes/rust-github-actions
https://github.com/marketplace/actions/rust-release-binary
https://docs.github.com/en/actions/using-workflows/about-workflows#creating-dependent-jobs
https://github.com/marketplace/actions/build-and-upload-rust-binary-to-github-releases
https://stackoverflow.com/questions/58139406/only-run-job-on-specific-branch-with-github-actions
https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run
https://github.com/orgs/community/discussions/26313
https://docs.github.com/en/actions/using-jobs/defining-outputs-for-jobs