- Notifications
You must be signed in to change notification settings - Fork0
GitHub action to use systemd-nspawn to run commands in a (un)booted container on a Raspberry Pi SD card image
License
Apache-2.0, Unknown licenses found
Licenses found
ethanjli/pinspawn-action
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
GitHub action to usesystemd-nspawn to run commands in a (un)booted container on a Raspberry Pi SD card image
systemd-nspawn isused to run commands in a light-weight namespace container, like chroot but with full virtualizationof the file system hierarchy, the process tree, the various IPC subsystems, and the host and domainname. It can also be used to boot the image's init program (which is usually systemd) as an OS; thisaction makes it easy to run a set of shell commands whether or not the OS is booted in thecontainer. You can use this action to set up Docker containers as part of your OS image buildprocess in GitHub Actions!
Note that currently only unbooted containers work correctly on GitHub's new hosted arm64 runners;booted systemd-nspawn containers spontaneously initiate shutdown as soon as the system boot sequencereaches the login prompt. Maybe that's a bug which will magically go away after the hosted arm64runners exit public preview (this is wishful thinking). If you want to start or interact with theDocker daemon inside an unbooted container on an arm64 runner, you will need instantiate thecontainer with theCAP_NET_ADMIN capability (to make iptables work as required by Docker) and thenmanually start both containerd (by launching/usr/bin/containerd as a background process) and theDocker daemon (by launching/usr/bin/dockerd as a background process). Seethe relevant example below for an illustration ofhow to do this.
Unlike thealternatives listed below (which you should evaluate based on your ownproject's requirements to see which ones might be more appropriate for you),ethanjli/pinspawn-action attempts to provide a bare-minimum abstraction which gets youcloserto shell scripting - it tries to minimize the amount of tool-specific abstraction for you to learn,and the only thing you can do with it is to run your own shell commands/scripts.
Also, by contrast to every below-listed alternative besides sdm, pinspawn-action takes advantage ofa mechanism which is more powerful (and more similar to actually-booted Raspberry Pi environments)than chroots. pinspawn-action is designed specifically as an ergonomic wrapper for GitHub Actionsto use systemd-nspawn with Raspberry Pi OS images.
-name:Install and run cowsayuses:ethanjli/pinspawn-action@v0.1.5with:image:rpi-os-image.imgrun:| apt-get update apt-get install -y cowsay /usr/games/cowsay 'I am running in a light-weight namespace container!'
-name:Run in Pythonuses:ethanjli/pinspawn-action@v0.1.5with:image:rpi-os-image.imgshell:pythonrun:| import platform for word in reversed(['!', platform.python_version(), 'Python', 'in', 'running', 'am', 'I']): print(word, end=' ')
-name:Run without root permissionsuses:ethanjli/pinspawn-action@v0.1.5with:image:rpi-os-image.imguser:pishell:shrun:| sudo apt-get update sudo apt-get install -y figlet figlet -f digital "I am $USER in $SHELL!"
-name:Make a script on the hostuses:1arp/create-a-file-action@0.4.5with:file:figlet.shcontent:| #!/usr/bin/env -S bash -eux figlet -f digital "I am $USER in $SHELL!"-name:Make the script executablerun:chmod a+x figlet.sh-name:Run script directlyuses:ethanjli/pinspawn-action@v0.1.5with:image:rpi-os-image.imgargs:--bind "$(pwd)":/run/externaluser:pishell:/run/external/figlet.sh
-name:Make a bootloader configuration snippetuses:1arp/create-a-file-action@0.4.5with:file:boot-config.snippetcontent:| # Enable support for the RV3028 RTC dtoverlay=i2c-rtc,rv3028,trickle-resistor-ohms=3000,backup-switchover-mode=1-name:Modify bootloader configurationuses:ethanjli/pinspawn-action@v0.1.5with:image:rpi-os-image.imgargs:--bind "$(pwd)":/run/externalrun:| cat /run/external/boot-config.snippet >> /boot/firmware/config.txt # Note: this assumes rpi-os-image.img is for bookworm or later: cp /boot/firmware/config.txt /run/external/boot.config-name:Print the bootloader configrun:cat boot.config
Note: the system in the container will shut down after the specified commands finish running.
-name:Analyze systemd boot processuses:ethanjli/pinspawn-action@v0.1.5with:image:rpi-os-image.imgargs:--bind "$(pwd)":/run/externalboot:truerun:| while ! systemd-analyze 2>/dev/null; do echo "Waiting for boot to finish..." sleep 5 done systemd-analyze critical-chain | cat systemd-analyze blame | cat systemd-analyze plot > /run/external/bootup-timeline.svg echo "Done!"-name:Upload the bootup timeline to Job Artifactsuses:actions/upload-artifact@v4with:name:bootup-timelinepath:bootup-timeline.svgif-no-files-found:erroroverwrite:true
Note: this example willonly work if you run it in theubuntu-24.04-arm GitHub Actions runner;trying to run it onubuntu-22.04-arm results in an error whendockerd tries to start(failed to start daemon: Devices cgroup isn't mounted).
-name:Install Dockeruses:ethanjli/pinspawn-action@v0.1.5with:image:rpi-os-image.imgrun:| export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y ca-certificates curl install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/debian \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ > /etc/apt/sources.list.d/docker.list apt-get update apt-get install -y \ docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin-name:Pull a Docker container imageuses:ethanjli/pinspawn-action@v0.1.5with:image:rpi-os-image.imgargs:--capability=CAP_NET_ADMINrun:| #!/bin/bash -eux /usr/bin/containerd & sleep 5 /usr/bin/dockerd & sleep 10 docker image pull hello-world docker image ls
Inputs:
| Input | Allowed values | Required? | Description |
|---|---|---|---|
image | file path | yes | Path of the image to use for the container. |
args | systemd-nspawn options/args | no (default ``) | Options, args, and/or a command to pass tosystemd-nspawn. |
shell | ``,bash, `sh`, `python`, etc. | no (default ``) | The shell to use for running commands. |
run | shell commands | no (default ``) | Commands to run in the shell. |
user | name of user in image | no (defaultroot) | The user to run commands as. |
boot | false,true | no (defaultfalse) | Boot the image's init program (usually systemd) as PID 1. |
run-service | file path | no (default ``) | systemd service to runshell with therun commands; only used with booted containers. |
boot-partition-mount | file path | no (default ``) | Mount point of the boot partition. |
imagemust be the path of an unmounted raw disk image (such as a Raspberry Pi OS SD card image),where partition 2 should be mounted as the root filesystem (i.e./) and partition 1 should bemounted to/boot.argscan be a list of command-line options/arguments forsystemd-nspawn.You should not set the--useror--bootflags here; instead, you should set theuserandbootaction inputs.If
runis not left empty,shellwill be used to execute commands specified in theruninput.You can use built-inshellkeywords, or you can define a custom set of shell options. The shellcommand that is run internally executes a temporary file that contains the commands to run, likein GitHub Actions.Please refer to the GitHub Actions semantics of theshellkeyword of job steps for detailsabout the behavior of this action'sshellinput.If you just want to run a single script, you can leave
runempty and provide that script as theshellinput. However, you will need to set the appropriate permissions on the script file.If
bootis enabled, this action will usesystemd-nspawnto automatically search for an initprogram in the image (typically systemd) and invoke it as PID 1, instead of a shell.The provided
runcommands will be triggered by a temporary system service defined with thefollowing template (unless you specify a different service file template using therun-serviceinput):[Unit]Description=Run commands in booted OSAfter=getty.target[Service]Type=execExecStart=bash -c "\ su - {user} -c '{command}; echo $? | tee {result}'; \ echo Shutting down...; \ shutdown now \" &StandardOutput=tty[Install]WantedBy=getty.targetThis service file template has string interpolation applied to the following strings:
{user}will be replaced with the value of the action'suserinput.{command}will be replaced with a command to run your specifiedruncommands using yourspecifiedshell{result}will be replaced with the path of a temporary file whose contents will be checkedafter the container finishes running to determine whether the command finished successfully(in which case the file should be the string0); this file is interpreted as holding areturn code.
If this flag is enabled, then any arguments specified as the command line in
argsare used asarguments for the init program, i.e.systemd-nspawnwill be invoked likesystemd-nspawn --boot {args}.
If
boot-partition-mountis not specified, the action will automatically select/boot/firmwareif that directory exists in the root partition (which is true of RPi OS bookworm or later), or/boototherwise (for compatibility with RPi OS bullseye).
You may also be able to run thegha-wrapper-pinspawn.sh script on your own computer, but you willhave to figure out how to install the required dependencies yourself - take a look ataction.yml to see what extra apt packages get installed on top of the GitHub Actionsrunner's default set of packages, and to see how you can pass inputs to thegha-wrapper-pinspawn.sh script as environment variables. Or, if youreally can't tolerate using environment variables, you can instead directly invokepinspawn.sh - look at the contents ofgha-wrapper-pinspawn.sh to see how to doso.
I'm aware of a variety of existing approaches for generating custom Raspberry Pi OS images in GitHubActions CI for building a custom OS which is meant to be maintained (i.e. changed) over time. Thefollowing are all built as abstractionsaway from pure shell-scripting and, with the exceptionof sdm, are based on pure chroots (which come with various limitations, some of which may affectyour work depending on your goals):
- Nature40/pimod: a great option to consider if you want to useDockerfile-style syntax. Ready-to-use as aGitHub Action! If you want to interact with Docker, you will need to use some advancedDocker-in-Docker magic - seehere fordetails.
- usimd/pi-gen-action withRPi-Distro/pi-gen: a good option to consider if you wantto build OS images using the same(rather-complicated) abstractionsystem that is used for building the Raspberry Pi OS, e.g. for multi-stage builds.
- guysoft/CustomPiOS: a system of build scripts organizedaround pre-defined modules which you can combine with your own scripts. A good option to considerif you want to use some of the modules they provide in your own OS image, or if you also want tobuild images locally (e.g. in a Docker container, apparently?).
- gitbls/sdm: a system of build scripts organized aroundpre-defined plugins which you can combine with your own scripts. Has many more plugins for you tosearch through compared to CustomPiOS, and also has enough functionality to replace Raspberry PiImager. Can work on chroots, but defaults to using systemd-nspawn instead. You may need to figureout GitHub Actions integration yourself.
- pndurette/pi-packer: potentially reasonable if you know(or would be comfortable learning)Packer andPacker HCL. You may need tofigure out GitHub Actions integration yourself.
- raspberrypi/rpi-image-gen: Raspberry Pi's newframework for building custom images, if you want to learn their unique YAML-based configurationsystem. You may need to figure out GitHub Actions integration yourself.
If you absolutely need to run shell commands/scripts in a booted QEMU virtual machine with fullvirtualization of Raspberry Pi hardware, I have createdethanjli/piqemu-action with basically the sameinterface as pinspawn-action. However, I found in GitHub Actions runners that Raspberry Pi QEMU VMsare quite slow (especially for downloading files over the network) and flaky (in the sense that theywill just freeze in the middle of work without a helpful error message, requiring you to restart theworkflow to make it work - which is definitely a some kind of bug). I strongly recommend usingpinspawn-action instead of piqemu-action unless you have something which absolutely won't workoutside a full virtual machine.
We have chosen the following licenses in order to give away our work for free, so that you canfreely use it for whatever purposes you have, with minimal restrictions, while still protecting ourdisclaimer that this work is provided without any warranties at all. If you're using this project,or if you have questions about the licenses, we'd love to hear from you - please start a newdiscussion thread in the "Discussions" tab of this repository on Github or email us atlietk12@gmail.com .
Except where otherwise indicated, source code provided here is covered by the following information:
Copyright Ethan Li and pinspawn-action contributors
SPDX-License-Identifier:Apache-2.0 OR BlueOak-1.0.0
Software files in this repository are released under theApache 2.0 License and theBlue Oak Model License 1.0.0;you can use the source code provided here either under the Apache License or under theBlue Oak Model License, and you get to decide which license you will agree to.We are making the software available under the Apache license because it'sOSI-approved,but we like the Blue Oak Model License more because it's easier to read and understand.Please read and understand the licenses for the specific language governing permissions andlimitations.
About
GitHub action to use systemd-nspawn to run commands in a (un)booted container on a Raspberry Pi SD card image
Topics
Resources
License
Apache-2.0, Unknown licenses found
Licenses found
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.