{"id":16631436,"url":"https://github.com/ethanjli/pinspawn-action","last_synced_at":"2025-04-14T20:12:18.096Z","repository":{"id":241591409,"uuid":"807168069","full_name":"ethanjli/pinspawn-action","owner":"ethanjli","description":"GitHub action to use systemd-nspawn to run commands in a (un)booted container on a Raspberry Pi SD card image","archived":false,"fork":false,"pushed_at":"2025-03-21T18:05:32.000Z","size":95,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-14T20:12:04.974Z","etag":null,"topics":["pi-image","raspberry-pi","sd-card","sd-card-image","sd-image","systemd-nspawn"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ethanjli.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-05-28T15:44:37.000Z","updated_at":"2025-03-21T18:05:35.000Z","dependencies_parsed_at":"2025-01-18T07:10:02.871Z","dependency_job_id":"67d9a606-826f-49c9-aca9-aaad2d4dc01d","html_url":"https://github.com/ethanjli/pinspawn-action","commit_stats":{"total_commits":74,"total_committers":1,"mean_commits":74.0,"dds":0.0,"last_synced_commit":"a11bbbf016044f4ca36b9e22bf7ee8882dbe7717"},"previous_names":["ethanjli/pinspawn-action"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ethanjli%2Fpinspawn-action","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ethanjli%2Fpinspawn-action/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ethanjli%2Fpinspawn-action/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ethanjli%2Fpinspawn-action/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ethanjli","download_url":"https://codeload.github.com/ethanjli/pinspawn-action/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248952350,"owners_count":21188426,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["pi-image","raspberry-pi","sd-card","sd-card-image","sd-image","systemd-nspawn"],"created_at":"2024-10-12T04:52:56.151Z","updated_at":"2025-04-14T20:12:18.089Z","avatar_url":"https://github.com/ethanjli.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PiNspawn GitHub Action\n\nGitHub action to use `systemd-nspawn` to run commands in a (un)booted container on a Raspberry Pi SD card image\n\n[`systemd-nspawn`](https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html) is\nused to run commands in a light-weight namespace container, like chroot but with full virtualization\nof the file system hierarchy, the process tree, the various IPC subsystems, and the host and domain\nname. It can also be used to boot the image's init program (which is usually systemd) as an OS; this\naction makes it easy to run a set of shell commands whether or not the OS is booted in the\ncontainer. You can use this action to set up Docker containers as part of your OS image build\nprocess in GitHub Actions!\n\nNote that currently only unbooted containers work correctly on GitHub's new hosted arm64 runners;\nbooted systemd-nspawn containers spontaneously initiate shutdown as soon as the system boot sequence\nreaches the login prompt. Maybe that's a bug which will magically go away after the hosted arm64\nrunners exit public preview (this is wishful thinking). If you want to start or interact with the\nDocker daemon inside an unbooted container on an arm64 runner, you will need instantiate the\ncontainer with the `CAP_NET_ADMIN` capability (to make iptables work as required by Docker) and then\nmanually start both containerd (by launching `/usr/bin/containerd` as a background process) and the\nDocker daemon (by launching `/usr/bin/dockerd` as a background process). See\n[the relevant example](#interact-with-docker-in-an-unbooted-container) below for an illustration of\nhow to do this.\n\n## Motivation\n\nUnlike the [alternatives](#alternatives) listed below (which you should evaluate based on your own\nproject's requirements to see which ones might be more appropriate for you),\n`ethanjli/pinspawn-action` attempts to provide a bare-minimum abstraction which gets you **closer**\nto shell scripting - it tries to minimize the amount of tool-specific abstraction for you to learn,\nand the only thing you can do with it is to run your own shell commands/scripts.\n\nAlso, by contrast to every below-listed alternative besides sdm, pinspawn-action takes advantage of\na mechanism which is more powerful (and more similar to actually-booted Raspberry Pi environments)\nthan chroots. pinspawn-action is designed specifically as an ergonomic wrapper for GitHub Actions\nto use systemd-nspawn with Raspberry Pi OS images. \n\n## Basic Usage Examples\n\n### Run shell commands as root\n\n```yaml\n- name: Install and run cowsay\n  uses: ethanjli/pinspawn-action@v0.1.4\n  with:\n    image: rpi-os-image.img\n    run: |\n      apt-get update\n      apt-get install -y cowsay\n      /usr/games/cowsay 'I am running in a light-weight namespace container!'\n```\n\n### Run shell commands in a specific shell\n\n```yaml\n- name: Run in Python\n  uses: ethanjli/pinspawn-action@v0.1.4\n  with:\n    image: rpi-os-image.img\n    shell: python\n    run: |\n      import platform\n\n      for word in reversed(['!', platform.python_version(), 'Python', 'in', 'running', 'am', 'I']):\n        print(word, end=' ')\n```\n\n### Run shell commands as the `pi` user\n\n```yaml\n- name: Run without root permissions\n  uses: ethanjli/pinspawn-action@v0.1.4\n  with:\n    image: rpi-os-image.img\n    user: pi\n    shell: sh\n    run: |\n      sudo apt-get update\n      sudo apt-get install -y figlet\n      figlet -f digital \"I am $USER in $SHELL!\"\n```\n\n### Run an external script directly, with the shell selected by its shebang line\n\n```yaml\n- name: Make a script on the host\n  uses: 1arp/create-a-file-action@0.4.5\n  with:\n    file: figlet.sh\n    content: |\n      #!/usr/bin/env -S bash -eux\n      figlet -f digital \"I am $USER in $SHELL!\"\n\n- name: Make the script executable\n  run: chmod a+x figlet.sh\n\n- name: Run script directly\n  uses: ethanjli/pinspawn-action@v0.1.4\n  with:\n    image: rpi-os-image.img\n    args: --bind \"$(pwd)\":/run/external\n    user: pi\n    shell: /run/external/figlet.sh\n```\n\n### Run shell commands with one or more bind mounts from the host OS\n\n```yaml\n- name: Make a bootloader configuration snippet\n  uses: 1arp/create-a-file-action@0.4.5\n  with:\n    file: boot-config.snippet\n    content: |\n      # Enable support for the RV3028 RTC\n      dtoverlay=i2c-rtc,rv3028,trickle-resistor-ohms=3000,backup-switchover-mode=1\n\n- name: Modify bootloader configuration\n  uses: ethanjli/pinspawn-action@v0.1.4\n  with:\n    image: rpi-os-image.img\n    args: --bind \"$(pwd)\":/run/external\n    run: |\n      cat /run/external/boot-config.snippet \u003e\u003e /boot/config.txt\n      cp /boot/config.txt /run/external/boot.config\n\n- name: Print the bootloader config\n  run: cat boot.config\n```\n\n### Run shell commands in a booted container\n\nNote: the system in the container will shut down after the specified commands finish running.\n\n```yaml\n- name: Analyze systemd boot process\n  uses: ethanjli/pinspawn-action@v0.1.4\n  with:\n    image: rpi-os-image.img\n    args: --bind \"$(pwd)\":/run/external\n    boot: true\n    run: |\n      while ! systemd-analyze 2\u003e/dev/null; do\n        echo \"Waiting for boot to finish...\"\n        sleep 5\n      done\n      systemd-analyze critical-chain | cat\n      systemd-analyze blame | cat\n      systemd-analyze plot \u003e /run/external/bootup-timeline.svg\n      echo \"Done!\"\n\n- name: Upload the bootup timeline to Job Artifacts\n  uses: actions/upload-artifact@v4\n  with:\n    name: bootup-timeline\n    path: bootup-timeline.svg\n    if-no-files-found: error\n    overwrite: true\n```\n\n### Interact with Docker in an unbooted container\n\nNote: this example will *only* work if you run it in the `ubuntu-24.04-arm` GitHub Actions runner;\ntrying to run it on `ubuntu-22.04-arm` results in an error when `dockerd` tries to start\n(`failed to start daemon: Devices cgroup isn't mounted`).\n\n```yaml\n- name: Install Docker\n  uses: ethanjli/pinspawn-action@v0.1.4\n  with:\n    image: rpi-os-image.img\n    run: |\n      export DEBIAN_FRONTEND=noninteractive\n      apt-get update\n      apt-get install -y ca-certificates curl\n      install -m 0755 -d /etc/apt/keyrings\n      curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc\n      chmod a+r /etc/apt/keyrings/docker.asc\n      echo \\\n        \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \\\n        https://download.docker.com/linux/debian \\\n        $(. /etc/os-release \u0026\u0026 echo \"$VERSION_CODENAME\") stable\" \\\n        \u003e /etc/apt/sources.list.d/docker.list\n      apt-get update\n      apt-get install -y \\\n        docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n\n- name: Pull a Docker container image\n  uses: ethanjli/pinspawn-action@v0.1.4\n  with:\n    image: rpi-os-image.img\n    args: --capability=CAP_NET_ADMIN\n    run: |\n      #!/bin/bash -eux\n\n      /usr/bin/containerd \u0026\n      sleep 5\n      /usr/bin/dockerd \u0026\n      sleep 10\n\n      docker image pull hello-world\n      docker image ls\n```\n\n## Usage Options\n\nInputs:\n\n| Input         | Allowed values                   | Required?            | Description                                                                               |\n|---------------|----------------------------------|----------------------|-------------------------------------------------------------------------------------------|\n| `image`       | file path                        | yes                  | Path of the image to use for the container.                                               |\n| `args`        | `systemd-nspawn` options/args    | no (default ``)      | Options, args, and/or a command to pass to `systemd-nspawn`.                              |\n| `shell`       | ``, `bash`, `sh`, `python`, etc. | no (default ``)      | The shell to use for running commands.                                                    |\n| `run`         | shell commands                   | no (default ``)      | Commands to run in the shell.                                                             |\n| `user`        | name of user in image            | no (default `root`)  | The user to run commands as.                                                              |\n| `boot`        | `false`, `true`                  | no (default `false`) | Boot the image's init program (usually systemd) as PID 1.                                 |\n| `run-service` | file path                        | no (default ``)      | systemd service to run `shell` with the `run` commands; only used with booted containers. |\n\n- `image` must be the path of an unmounted raw disk image (such as a Raspberry Pi OS SD card image),\n  where partition 2 should be mounted as the root filesystem (i.e. `/`) and partition 1 should be\n  mounted to `/boot`.\n\n- `args` can be a list of command-line options/arguments for\n  [`systemd-nspawn`](https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html).\n  You should not set the `--user` or `--boot` flags here; instead, you should set the `user` and\n  `boot` action inputs.\n\n- If `run` is not left empty, `shell` will be used to execute commands specified in the `run` input.\n  You can use built-in `shell` keywords, or you can define a custom set of shell options. The shell\n  command that is run internally executes a temporary file that contains the commands to run, like\n  [in GitHub Actions](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell).\n  Please refer to the GitHub Actions semantics of the `shell` keyword of job steps for details\n  about the behavior of this action's `shell` input.\n\n  If you just want to run a single script, you can leave `run` empty and provide that script as the\n  `shell` input. However, you will need to set the appropriate permissions on the script file.\n\n- If `boot` is enabled, this action will use `systemd-nspawn` to automatically search for an init\n  program in the image (typically systemd) and invoke it as PID 1, instead of a shell.\n\n  - The provided `run` commands will be triggered by a temporary system service defined with the\n    following template (unless you specify a different service file template using the `run-service`\n    input):\n\n    ```\n    [Unit]\n    Description=Run commands in booted OS\n    After=getty.target\n\n    [Service]\n    Type=exec\n    ExecStart=bash -c \"\\\n      su - {user} -c '{command}; echo $? | tee {result}'; \\\n      echo Shutting down...; \\\n      shutdown now \\\n    \" \u0026\n    StandardOutput=tty\n\n    [Install]\n    WantedBy=getty.target\n    ```\n\n    This service file template has string interpolation applied to the following strings:\n\n    - `{user}` will be replaced with the value of the action's `user` input.\n    - `{command}` will be replaced with a command to run your specified `run` commands using your\n      specified `shell`\n    - `{result}` will be replaced with the path of a temporary file whose contents will be checked\n      after the container finishes running to determine whether the command finished successfully\n      (in which case the file should be the string `0`); this file is interpreted as holding a\n      return code.\n\n  - If this flag is enabled, then any arguments specified as the command line in `args` are used as\n    arguments for the init program, i.e. `systemd-nspawn` will be invoked like\n    `systemd-nspawn --boot {args}`.\n\n## Running Locally\n\nYou may also be able to run the `gha-wrapper-pinspawn.sh` script on your own computer, but you will\nhave to figure out how to install the required dependencies yourself - take a look at\n[action.yml](./action.yml) to see what extra apt packages get installed on top of the GitHub Actions\nrunner's default set of packages, and to see how you can pass inputs to the\n[gha-wrapper-pinspawn.sh](./gha-wrapper-pinspawn.sh) script as environment variables. Or, if you\nreally can't tolerate using environment variables, you can instead directly invoke\n[pinspawn.sh](./pinspawn.sh) - look at the contents of `gha-wrapper-pinspawn.sh` to see how to do\nso.\n\n## Alternatives\n\nI'm aware of a variety of existing approaches for generating custom Raspberry Pi OS images in GitHub\nActions CI for building a custom OS which is meant to be maintained (i.e. changed) over time. The\nfollowing are all built as abstractions **away** from pure shell-scripting and, with the exception\nof sdm, are based on pure chroots (which come with various limitations, some of which may affect\nyour work depending on your goals):\n\n- [Nature40/pimod](https://github.com/Nature40/pimod): a great option to consider if you want to use\n  [Dockerfile](https://docs.docker.com/build/concepts/dockerfile/)-style syntax. Ready-to-use as a\n  GitHub Action! If you want to interact with Docker, you will need to use some advanced\n  Docker-in-Docker magic - see\n  [here](https://github.com/PlanktoScope/PlanktoScope/issues/42#issuecomment-2132049469) for\n  details.\n- [usimd/pi-gen-action](https://github.com/usimd/pi-gen-action) with\n  [RPi-Distro/pi-gen](https://github.com/RPi-Distro/pi-gen): a good option to consider if you want\n  to build OS images using the same\n  ([rather-complicated](https://opensource.com/article/21/7/custom-raspberry-pi-image)) abstraction\n  system that is used for building the Raspberry Pi OS, e.g. for multi-stage builds.\n- [guysoft/CustomPiOS](https://github.com/guysoft/CustomPiOS): a system of build scripts organized\n  around pre-defined modules which you can combine with your own scripts. A good option to consider\n  if you want to use some of the modules they provide in your own OS image, or if you also want to\n  build images locally (e.g. in a Docker container, apparently?).\n- [gitbls/sdm](https://github.com/gitbls/sdm): a system of build scripts organized around\n  pre-defined plugins which you can combine with your own scripts. Has many more plugins for you to\n  search through compared to CustomPiOS, and also has enough functionality to replace Raspberry Pi\n  Imager. Can work on chroots, but defaults to using systemd-nspawn instead. You may need to figure\n  out GitHub Actions integration yourself.\n- [pndurette/pi-packer](https://github.com/pndurette/pi-packer): potentially reasonable if you know\n  (or would be comfortable learning) [Packer](https://www.packer.io/) and\n  [Packer HCL](https://developer.hashicorp.com/packer/docs/templates/hcl_templates). You may need to\n  figure out GitHub Actions integration yourself.\n- [raspberrypi/rpi-image-gen](https://github.com/raspberrypi/rpi-image-gen): Raspberry Pi's new\n  framework for building custom images, if you want to learn their unique YAML-based configuration\n  system. You may need to figure out GitHub Actions integration yourself.\n\nIf you absolutely need to run shell commands/scripts in a booted QEMU virtual machine with full\nvirtualization of Raspberry Pi hardware, I have created\n[ethanjli/piqemu-action](https://github.com/ethanjli/piqemu-action) with basically the same\ninterface as pinspawn-action. However, I found in GitHub Actions runners that Raspberry Pi QEMU VMs\nare quite slow (especially for downloading files over the network) and flaky (in the sense that they\nwill just freeze in the middle of work without a helpful error message, requiring you to restart the\nworkflow to make it work - which is definitely a some kind of bug). I strongly recommend using\npinspawn-action instead of piqemu-action unless you have something which absolutely won't work\noutside a full virtual machine.\n\n## Licensing\n\nWe have chosen the following licenses in order to give away our work for free, so that you can\nfreely use it for whatever purposes you have, with minimal restrictions, while still protecting our\ndisclaimer that this work is provided without any warranties at all. If you're using this project,\nor if you have questions about the licenses, we'd love to hear from you - please start a new\ndiscussion thread in the \"Discussions\" tab of this repository on Github or email us at\n\u003clietk12@gmail.com\u003e .\n\n### Software\n\nExcept where otherwise indicated, source code provided here is covered by the following information:\n\n**Copyright Ethan Li and pinspawn-action contributors**\n\nSPDX-License-Identifier: `Apache-2.0 OR BlueOak-1.0.0`\n\nSoftware files in this repository are released under the\n[Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0) and the\n[Blue Oak Model License 1.0.0](https://blueoakcouncil.org/license/1.0.0);\nyou can use the source code provided here either under the Apache License or under the\nBlue Oak Model License, and you get to decide which license you will agree to.\nWe are making the software available under the Apache license because it's\n[OSI-approved](https://writing.kemitchell.com/2019/05/05/Rely-on-OSI.html),\nbut we like the Blue Oak Model License more because it's easier to read and understand.\nPlease read and understand the licenses for the specific language governing permissions and\nlimitations.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fethanjli%2Fpinspawn-action","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fethanjli%2Fpinspawn-action","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fethanjli%2Fpinspawn-action/lists"}