Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: build-os | |
| on: | |
| workflow_call: | |
| inputs: | |
| name: | |
| description: 'The name of the OS image to build' | |
| required: true | |
| type: string | |
| base_release_name: | |
| description: 'The release name of the RPi OS base image (bullseye, bookworm)' | |
| required: true | |
| type: string | |
| base_image_variant: | |
| description: 'The name of the RPi OS base image variant (lite, desktop, or full)' | |
| required: true | |
| type: string | |
| base_release_date: | |
| description: 'The release date of the RPi OS base image' | |
| required: true | |
| type: string | |
| base_file_release_date: | |
| description: 'The release date of the RPi OS base image file, if different from the group release date' | |
| required: false | |
| type: string | |
| arch: | |
| description: 'The CPU architecture of the OS (armhf, arm64)' | |
| required: true | |
| type: string | |
| jobs: | |
| build-os-image: | |
| name: Build image | |
| runs-on: ubuntu-24.04-arm | |
| env: | |
| SETUP_USER: pi | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Get actual repo & commit SHA | |
| run: | | |
| # Get the actual repo name on PRs from external forks of the repo: | |
| if [ -n "${{ github.event.pull_request.head.repo.full_name }}" ]; then | |
| echo "REPO=${{ github.event.pull_request.head.repo.full_name }}" >> $GITHUB_ENV | |
| else | |
| echo "REPO=${{ github.repository }}" >> $GITHUB_ENV | |
| fi | |
| # Get the SHA of the actual commit (not the fake merge commit) on PR-triggered runs | |
| # (refer to https://stackoverflow.com/a/68068674/23202949): | |
| if [ -n "${{ github.event.pull_request.head.sha }}" ]; then | |
| echo "ACTUAL_SHA=${{ github.event.pull_request.head.sha }}" >> $GITHUB_ENV | |
| else | |
| # Note: we want to have a SHA to check out even in merge queues, so we always fall back | |
| # to a SHA even if it's a fictional SHA detached from any branches: | |
| echo "ACTUAL_SHA=${{ github.sha }}" >> $GITHUB_ENV | |
| fi | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| # PR-triggered workflow runs have a merge commit which is detached from existing | |
| # branches and messes up pseudoversion determination when we record installer versioning | |
| # information, so instead we check out the actual commit (and fetch all tags so we can | |
| # determine a pseudoversion string): | |
| repository: ${{ env.REPO }} | |
| ref: ${{ env.ACTUAL_SHA }} | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| filter: 'blob:none' | |
| submodules: recursive | |
| # GET BASE IMAGE | |
| - name: Determine Raspberry Pi OS base image URL | |
| id: rpi-os-image | |
| run: | | |
| case '${{ inputs.base_release_name }}' in | |
| 'bookworm') | |
| IMAGE_RELEASE_CHANNEL='raspios' | |
| ;; | |
| 'bullseye') | |
| IMAGE_RELEASE_CHANNEL='raspios_oldstable' | |
| ;; | |
| *) | |
| echo "Unknown release name: ${{ inputs.base_release_name }}" | |
| exit 1 | |
| ;; | |
| esac | |
| IMAGE_REPO_GROUP="$IMAGE_RELEASE_CHANNEL" | |
| if [[ "${{ inputs.base_image_variant }}" != "desktop" ]]; then | |
| IMAGE_REPO_GROUP="${IMAGE_REPO_GROUP}_${{ inputs.base_image_variant }}" | |
| fi | |
| IMAGE_REPO_GROUP="${IMAGE_REPO_GROUP}_${{ inputs.arch }}" | |
| if [[ -z "${{ inputs.base_file_release_date }}" ]]; then | |
| BASE_FILE_RELEASE_DATE="${{ inputs.base_release_date }}" | |
| else | |
| BASE_FILE_RELEASE_DATE="${{ inputs.base_file_release_date }}" | |
| fi | |
| IMAGE_FILENAME="$BASE_FILE_RELEASE_DATE-raspios-${{ inputs.base_release_name }}-${{ inputs.arch }}" | |
| if [[ "${{ inputs.base_image_variant }}" != "desktop" ]]; then | |
| IMAGE_FILENAME="${IMAGE_FILENAME}-${{ inputs.base_image_variant }}" | |
| fi | |
| IMAGE_FILENAME="${IMAGE_FILENAME}.img.xz" | |
| IMAGE_URL="https://downloads.raspberrypi.com/$IMAGE_REPO_GROUP/images/$IMAGE_REPO_GROUP-${{ inputs.base_release_date }}/$IMAGE_FILENAME" | |
| echo "RPi OS image filename: $IMAGE_FILENAME" | |
| echo "image_filename=$IMAGE_FILENAME" >> $GITHUB_OUTPUT | |
| echo "RPi OS image URL: $IMAGE_URL" | |
| echo "image_url=$IMAGE_URL" >> $GITHUB_OUTPUT | |
| - name: Download and cache base image | |
| id: download-base | |
| uses: ethanjli/cached-download-action@v0.1.2 | |
| with: | |
| url: ${{ steps.rpi-os-image.outputs.image_url }} | |
| destination: /tmp/${{ steps.rpi-os-image.outputs.image_filename }} | |
| - name: Decompress & grow base image | |
| id: expand-image | |
| uses: ethanjli/pigrow-action@v0.1.1 | |
| with: | |
| image: ${{ steps.download-base.outputs.destination }} | |
| mode: to | |
| size: 16G | |
| # RECORD INSTALLER VERSIONING INFORMATION | |
| # The user-driven setup script (install.planktoscope.community/distro.sh) records version info | |
| # which is read by the PlanktoScope's Python backend and Node-RED dashboard, so we must | |
| # record the same information here. | |
| - name: Record OS installation versioning information | |
| uses: ethanjli/pinspawn-action@v0.1.5 | |
| with: | |
| image: ${{ steps.expand-image.outputs.destination }} | |
| user: ${{ env.SETUP_USER }} | |
| args: >- | |
| --bind "$(pwd)":/run/os-setup | |
| --machine=raspberrypi | |
| run: | | |
| export DEBIAN_FRONTEND=noninteractive | |
| sudo apt-get update -y -o Dpkg::Progress-Fancy=0 | |
| sudo apt-get install -y -o Dpkg::Progress-Fancy=0 git | |
| # chown is needed to suppress a git error about dubious repo ownership: | |
| sudo chown -R $USER "/run/os-setup" | |
| set -x | |
| ACTUAL_SHA="$(printf "%.7s" "${{ env.ACTUAL_SHA }}")" | |
| /run/os-setup/os/ci-record-version.sh \ | |
| "github.com/${{ env.REPO }}" "$ACTUAL_SHA" "hash" \ | |
| "/run/os-setup" | |
| echo "installer-config.yml:" | |
| cat /usr/share/planktoscope/installer-config.yml | |
| echo "installer-versioning.yml:" | |
| cat /usr/share/planktoscope/installer-versioning.yml | |
| # If we don't do this, then the post-job cleanup for the actions/checkout step will emit a | |
| # warning: | |
| - name: Reset owner of Git repo | |
| run: | | |
| sudo chown -R $USER . | |
| # Note: the following step isn't strictly necessary, but it's nice to separate the GitHub | |
| # Actions log outputs for setting up pinspawn-action for the first time from the logs for | |
| # pre-downloading container images | |
| - name: Install pinspawn-action dependencies | |
| uses: ethanjli/pinspawn-action@v0.1.5 | |
| with: | |
| image: ${{ steps.expand-image.outputs.destination }} | |
| run: echo "Done!" | |
| # RUN OS SETUP SCRIPTS | |
| - name: Run OS setup scripts in an unbooted container | |
| uses: ethanjli/pinspawn-action@v0.1.5 | |
| with: | |
| image: ${{ steps.expand-image.outputs.destination }} | |
| user: ${{ env.SETUP_USER }} | |
| # Note: CAP_NET_ADMIN is needed for iptables, which is needed for Docker (at least in an | |
| # unbooted container). Setting the machine ID (and therefore hostname) to `raspberrypi` | |
| # resolves noisy (but harmless) error messages from sudo. | |
| args: >- | |
| --bind "$(pwd)":/run/os-setup | |
| --capability=CAP_NET_ADMIN | |
| --machine=raspberrypi | |
| run: | | |
| echo "Running setup scripts..." | |
| export DEBIAN_FRONTEND=noninteractive | |
| /run/os-setup/os/setup.sh | |
| echo "Done!" | |
| - name: Prepare for a headless first boot on bare metal | |
| uses: ethanjli/pinspawn-action@v0.1.5 | |
| env: | |
| DEFAULT_PASSWORD: copepode | |
| DEFAULT_KEYBOARD_LAYOUT: us | |
| with: | |
| image: ${{ steps.expand-image.outputs.destination }} | |
| args: --machine=raspberrypi | |
| run: | | |
| # Change default settings for the SD card to enable headless & keyboardless first boot | |
| # Note: we could change the username by making a `userconf.txt` file with the new | |
| # username and an encrypted representation of the password (and un-disabling and | |
| # unmasking `userconfig.service`), but we don't need to do that for now. | |
| # See https://github.com/RPi-Distro/userconf-pi/blob/bookworm/userconf-service and | |
| # https://www.raspberrypi.com/documentation/computers/configuration.html#configuring-a-user | |
| # and the "firstrun"-related and "cloudinit"-related lines of | |
| # https://github.com/raspberrypi/rpi-imager/blob/qml/src/OptionsPopup.qml and | |
| # the RPi SD card image's `/usr/lib/raspberrypi-sys-mods/firstboot` and | |
| # `/usr/lib/raspberrypi-sys-mods/imager_custom` scripts | |
| echo "pi:${{ env.DEFAULT_PASSWORD }}" | chpasswd | |
| sed -i \ | |
| -e "s~^XKBLAYOUT=.*~XKBLAYOUT=\"${{ env.DEFAULT_KEYBOARD_LAYOUT }}\"~" \ | |
| /etc/default/keyboard | |
| systemctl disable userconfig.service | |
| # This is needed to have the login prompt on tty1 (so that a user with a keyboard can | |
| # log in without switching away from the default tty), because we disabled | |
| # userconfig.service. See | |
| # https://forums.raspberrypi.com/viewtopic.php?p=2032694#p2032694 | |
| systemctl enable getty@tty1 | |
| # RUN TESTS | |
| - name: Run tests | |
| - uses: ethanjli/pinspawn-action@v0.1.5 | |
| run: | | |
| cd /home/pi/PlanktoScope | |
| just ci | |
| # UPLOAD OS IMAGE | |
| - name: Determine output image name | |
| env: | |
| TAG_PREFIX: 'software/v' | |
| run: | | |
| # Determine the version for the image name | |
| if [[ -n "${{ github.event.pull_request.head.sha }}" ]]; then | |
| # We're in a pull request | |
| version="$(\ | |
| printf "pr-%s-%.7s" \ | |
| "${{ github.event.pull_request.number }}" \ | |
| "${{ github.event.pull_request.head.sha }}" \ | |
| )" | |
| elif [[ "${{ github.ref_type }}" == "tag" && "${{ github.ref }}" == refs/tags/$TAG_PREFIX* ]]; then | |
| version="${{ github.ref }}" | |
| version="v${version#"refs/tags/$TAG_PREFIX"}" | |
| else | |
| version="$(printf "%.7s" "${{ github.sha }}")" | |
| fi | |
| variant="" | |
| if [[ "${{ inputs.base_release_name }}" != "bookworm" ]]; then | |
| variant="${{ inputs.base_release_name }}" | |
| fi | |
| if [[ "${{ inputs.arch }}" != "arm64" ]]; then | |
| variant="$variant.${{ inputs.arch }}" | |
| fi | |
| if [[ "${{ inputs.base_image_variant }}" != "lite" ]]; then | |
| variant="$variant.${{ inputs.base_image_variant }}" | |
| fi | |
| # Assemble the image name | |
| output_name="${{ inputs.name }}-$version" | |
| if [[ $variant != "" ]]; then | |
| output_name="$output_name.$variant" | |
| fi | |
| echo "OUTPUT_IMAGE_NAME=$output_name" >> $GITHUB_ENV | |
| - name: Shrink the OS image | |
| uses: ethanjli/pishrink-action@v0.1.4 | |
| env: | |
| PISHRINK_XZ: -T0 ${{ github.ref_type == 'tag' && '-9' || '-1' }} | |
| with: | |
| image: ${{ steps.expand-image.outputs.destination }} | |
| destination: ${{ env.OUTPUT_IMAGE_NAME }}.img | |
| compress: xz | |
| compress-parallel: true | |
| - name: Upload image to Job Artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.OUTPUT_IMAGE_NAME }} | |
| path: ${{ env.OUTPUT_IMAGE_NAME }}.img.xz | |
| if-no-files-found: error | |
| retention-days: 0 | |
| compression-level: 0 | |
| overwrite: true |