Skip to content

feat: example for build matrixing with RCL #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/100-build-rcl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: bluebuild
on:
push:
paths:
- "100-rcl/**"
- ".github/workflows/100-build-rcl.yml"
branches:
- main

pull_request:
workflow_dispatch: # allow manually triggering builds
jobs:
rcl:
name: Build recipe files with rcl
runs-on: ubuntu-latest
outputs:
recipe-matrix: ${{ steps.rcl.outputs.recipe-matrix }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install rcl
run: |
git clone https://github.com/ruuda/rcl.git /tmp/rcl
cd /tmp/rcl && cargo build --release
sudo cp target/release/rcl /usr/local/bin/

- name: Build recipe files
id: rcl
run: |
RCL_OUTPUT=$(rcl build ./100-rcl/recipes.rcl)
RECIPE_FILENAMES=$(echo "$RCL_OUTPUT" | awk '{ print $2 }' | sed -e 's/^recipes\///')
RECIPE_FILENAMES_JSON=$(echo "$RECIPE_FILENAMES" | jq --compact-output --raw-input '[inputs]')
MATRIX_JSON=$(jq -cn --argjson recipes "${RECIPE_FILENAMES_JSON[@]}" '{ recipes: $recipes }')

echo "recipe-matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT

- uses: actions/upload-artifact@v4
with:
name: recipes
path: ./100-rcl/recipes/
bluebuild:
name: Build Custom Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
needs: rcl
strategy:
fail-fast: false # stop GH from cancelling all matrix builds if one fails
matrix: ${{ fromJson(needs.rcl.outputs.recipe-matrix) }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: recipes
path: ./100-rcl/recipes/

- name: Build Custom Image
uses: blue-build/[email protected]
with:
recipe: ${{ matrix.recipes }}
working_directory: ./100-rcl
cosign_private_key: ${{ secrets.SIGNING_SECRET }}
registry_token: ${{ github.token }}
pr_event_number: ${{ github.event.number }}
skip_checkout: true
1 change: 1 addition & 0 deletions 100-rcl/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.yml
13 changes: 13 additions & 0 deletions 100-rcl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Easy build matrixing with [RCL](https://rcl-lang.org/)

[RCL](https://rcl-lang.org/) is a next-gen configuration language built in Rust which in this context can be used for easy matrixed recipe generation and image building through its simple way of generating configuration files. RCL is a superset of JSON and functions similarly to Jsonnet and Nix, but has a much better syntax and user experience.

The benefits of using a configuration language with built-in matrixing and templating is that you can easily generate multiple similar recipes using ex. different base images and easily introduce differences between them using simple `if` statements and template strings. You could get to the same result by simply using multiple recipe files, one for each image, but when building a lot of very similar images that approach might be cumbersome and require a ton of repetitive work.

This example showcases how to use RCL to generate a bunch recipes and build them all concurrently using with the GitHub Actions build matrix. The build matrix is automatically generated from the output of the `rcl build` command, and the logic of that generation might have to be adjusted to future RCL updates.

- See [recipes.rcl](./recipes.rcl) for the RCL configuration.
- See [build.yml](./build.yml) for the GitHub Actions workflow.

When working with a third-party configuration language, it is recommended to use the importing feature of the language instead of `from-file`, such that you can leave the `recipes/` directory completely empty or even uncreated. If you still want to use `from-file`, remember that the paths are not relative to the `.rcl` file, but to the `recipes/` directory.
See RCL's [documentation for file imports](https://docs.ruuda.nl/rcl/imports/).
4 changes: 4 additions & 0 deletions 100-rcl/cosign.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEliM5WSdh8GEB5c9ffB+/RJJAbAkT
Rkdfr9RsoSjPITV0foGyvoiQ06DCiaTgaj6kBG2rUHPXEgIH5B2IlPJMRw==
-----END PUBLIC KEY-----
4 changes: 4 additions & 0 deletions 100-rcl/files/scripts/nvidia-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -oue pipefail

echo "Something nvidia related I guess"
Empty file added 100-rcl/files/system/.gitkeep
Empty file.
Empty file.
67 changes: 67 additions & 0 deletions 100-rcl/recipes.rcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
let project = {
name = "test",
description = "My test project.",
base-images = "ghcr.io/ublue-os"
};

let gts-version = 40;

{
for base-image in ["kinoite", "silverblue"]:
for nvidia in [true, false]:
for image-version in [gts-version, "latest"]:

let nvidia-suffix = if nvidia: "-nvidia" else: "-main";

f"recipes/recipe-{base-image}{nvidia-suffix}-{image-version}.yml": {
format = "json",
contents = {
name = f"{project.name}-{base-image}{nvidia-suffix}",
description =
f"{project.description} ({base-image}{nvidia-suffix} edition, {if image-version == gts-version: "GTS" else "latest"} version)",

base-image = f"{project.base-images}/{base-image}{nvidia-suffix}",
image-version = image-version,

if image-version == gts-version:
alt-tags = [
"gts"
],

modules = [
{
type = "files",
files = [
{
source = "system",
destination = "/",
},
if nvidia:
{
source = "system_nvidia",
destination = "/",
}
]
},
{
type = "default-flatpaks",
user = {
install = [
"org.kde.krita",
"org.fedoraproject.MediaWriter",
if base-image == "kinoite":
"org.kde.elisa",
if base-image == "silverblue":
"com.github.neithern.g4music",
]
}
},
if nvidia: {
type = "script",
scripts = ["nvidia-setup.sh"]
},
{ type = "signing" }
]
}
}
}
Loading