Skip to content
Draft
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
28 changes: 27 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
# Verifies the ci action works against the fixture package in fixtures/ros_buildfarm_test/
# Verifies each action works against the fixture package in fixtures/ros_buildfarm_test/

name: CI

Expand Down Expand Up @@ -35,3 +35,29 @@ jobs:
os_code_name: ${{ matrix.os_code_name }}
source_dir: ${{ github.workspace }}/fixtures/ros_buildfarm_test
abort_on_test_failure: 'true'

test-package:
name: package / ${{ matrix.ros_distro }} / ${{ matrix.os_code_name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- ros_distro: humble
os_code_name: jammy
- ros_distro: kilted
os_code_name: noble
- ros_distro: jazzy
os_code_name: noble
- ros_distro: lyrical
os_code_name: resolute
- ros_distro: rolling
os_code_name: resolute
steps:
- uses: actions/checkout@v4

- uses: ./package
with:
ros_distro: ${{ matrix.ros_distro }}
os_code_name: ${{ matrix.os_code_name }}
source_dir: ${{ github.workspace }}/fixtures/ros_buildfarm_test
27 changes: 18 additions & 9 deletions Justfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
docker_socket_gid := `stat -c %g /var/run/docker.sock`

# act runs the job inside a container. The ros_buildfarm generates temp dirs
# under /tmp/ and spawns sibling Docker containers (via the host socket) with
# volume mounts to those paths. The host daemon resolves those paths on the
# HOST, not inside the act container -- so /tmp must be shared to make them
# match. --user avoids the buildfarm's uid==0 assertion. --group-add grants
# Docker socket access to the non-root user.
act_container_opts := "--user=" + `id -u` + ":" + `id -g` \
# CI action: --user avoids the buildfarm's uid==0 assertion in prerelease.sh.
# /tmp shared so Docker volume mounts resolve on the host (not the act container).
# --group-add grants Docker socket access to the non-root user.
ci_act_opts := "--user=" + `id -u` + ":" + `id -g` \
+ " --group-add=" + docker_socket_gid \
+ " -v /tmp:/tmp"

# Package action: runs as root in act (uid 1000 lacks passwordless sudo in the
# catthehacker image). The script handles the uid==0 restriction internally
# by su-ing to a build user for the ros_buildfarm calls that assert uid != 0.
package_act_opts := "--group-add=" + docker_socket_gid \
+ " -v /tmp:/tmp"

# Run the CI action locally against the fixture package for a given distro.
# Usage: just ci humble
ci distro:
act -j test-ci --matrix ros_distro:{{distro}} \
--container-options "{{act_container_opts}}"
act --rm -j test-ci --matrix ros_distro:{{distro}} \
--container-options "{{ci_act_opts}}"

# Run the package action locally against the fixture package for a given distro.
# Usage: just package humble
package distro:
act --rm -j test-package --matrix ros_distro:{{distro}} \
--container-options "{{package_act_opts}}"
72 changes: 72 additions & 0 deletions package/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: ros_buildfarm package
description: >
Build .deb packages from local ROS package source without a GBP release
repository or rosdistro registration. Uses bloom to generate Debian
packaging metadata, dpkg-buildpackage -S for the source package, then
hands off to the ros_buildfarm binary Docker pipeline
(run_binarydeb_job.py --skip-download-sourcepkg). The binary build runs
in the same generated Docker container as the real buildfarm.

inputs:
source_dir:
description: >
Path to the directory containing the ROS package source (must contain
package.xml). Defaults to github.workspace. A copy is made before
modification so the original checkout is not altered.
required: false
default: ${{ github.workspace }}
ros_distro:
description: ROS distribution name (e.g. rolling, humble, jazzy)
required: true
os_name:
description: Operating system name
required: false
default: ubuntu
os_code_name:
description: OS version code name (e.g. noble, jammy)
required: true
arch:
description: Target architecture
required: false
default: amd64
skip_tests:
description: Pass --skip-tests to the binary build
required: false
default: 'false'
upload_debs:
description: Upload resulting .deb files as a GitHub Actions artifact
required: false
default: 'true'

outputs:
deb_dir:
description: Path to the directory containing all generated .deb files
value: ${{ steps.build.outputs.deb_dir }}

runs:
using: composite
steps:
- name: Setup ros_buildfarm
# uses: ros-tooling/action-ros-buildfarm/setup@main
uses: ./setup

- id: build
shell: bash
env:
CONFIG_URL: https://raw.githubusercontent.com/ros2/ros_buildfarm_config/ros2/index.yaml
ROS_DISTRO: ${{ inputs.ros_distro }}
OS_NAME: ${{ inputs.os_name }}
OS_CODE_NAME: ${{ inputs.os_code_name }}
ARCH: ${{ inputs.arch }}
SOURCE_DIR: ${{ inputs.source_dir }}
SKIP_TESTS: ${{ inputs.skip_tests }}
run: bash ${{ github.action_path }}/run_package.sh

- name: Upload deb artifacts
if: inputs.upload_debs == 'true' && always()
uses: actions/upload-artifact@v4
with:
name: debs-${{ inputs.ros_distro }}-${{ inputs.os_code_name }}-${{ inputs.arch }}
path: ${{ github.workspace }}/.ros_buildfarm_debs
if-no-files-found: warn
204 changes: 204 additions & 0 deletions package/run_package.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Polymath Robotics, Inc.
# SPDX-License-Identifier: Apache-2.0
# Build .deb packages using the ros_buildfarm binary Docker pipeline.
#
# Uses bloom to generate debian/ packaging metadata and dpkg-buildpackage -S
# for the source package, then hands off to the buildfarm binary Docker
# pipeline via run_binarydeb_job.py --skip-download-sourcepkg. The binary
# build runs in the same generated Docker container as the real buildfarm.
#
# All configuration is via environment variables:
#
# Required:
# ROS_DISTRO ROS distribution name (e.g. rolling, humble)
# OS_CODE_NAME Ubuntu code name (e.g. noble, jammy)
#
# Optional:
# CONFIG_URL Buildfarm config index URL (default: official ROS 2 config)
# OS_NAME OS name (default: ubuntu)
# ARCH Target architecture (default: amd64)
# SOURCE_DIR Package source directory (default: current directory)
# SKIP_TESTS Set to 'true' to pass --skip-tests to the binary build
#
# Outputs written to GITHUB_OUTPUT if that variable is set:
# deb_dir path to directory containing generated .deb files

set -euo pipefail

CONFIG_URL="${CONFIG_URL:-https://raw.githubusercontent.com/ros2/ros_buildfarm_config/ros2/index.yaml}"
OS_NAME="${OS_NAME:-ubuntu}"
ARCH="${ARCH:-amd64}"
SOURCE_DIR="${SOURCE_DIR:-${PWD}}"
SKIP_TESTS="${SKIP_TESTS:-false}"

: "${ROS_DISTRO:?ROS_DISTRO is required}"
: "${OS_CODE_NAME:?OS_CODE_NAME is required}"


WORK_DIR=$(mktemp -d)
PACKAGE_DIR="${WORK_DIR}/package_src"
BINARYPKG_DIR="${WORK_DIR}/binarydeb"
DOCKERFILE_CREATE_DIR="${WORK_DIR}/dockerfile_create"
DOCKERFILE_BUILD_DIR="${WORK_DIR}/dockerfile_build"
RBF_DIR="${WORK_DIR}/ros_buildfarm"
KEY_DIR="${WORK_DIR}/keys"

mkdir -p "${BINARYPKG_DIR}" "${DOCKERFILE_CREATE_DIR}" "${DOCKERFILE_BUILD_DIR}" "${KEY_DIR}"
cp -r "${SOURCE_DIR}/." "${PACKAGE_DIR}/"

# ── Setup ─────────────────────────────────────────────────────────────────────
echo "::group::Setup"

# Clone ros_buildfarm source -- Docker containers expect it at /tmp/ros_buildfarm.
# The pip-installed package doesn't preserve the scripts/ source tree layout.
git clone --depth 1 \
https://github.com/ros-infrastructure/ros_buildfarm.git \
"${RBF_DIR}"

# Get the ROS apt repository GPG key for the Docker build environment.
# Install ros2-apt-source (which manages the key), then export in ASCII armor.
# The Dockerfile template embeds this key via signed-by= (not apt-key).
ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest \
| grep -F "tag_name" | awk -F'"' '{print $4}')
# shellcheck source=/dev/null
. /etc/os-release
curl -L -o /tmp/ros2-apt-source.deb \
"https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.${UBUNTU_CODENAME:-${VERSION_CODENAME}}_all.deb"
sudo dpkg -i /tmp/ros2-apt-source.deb
gpg --no-default-keyring \
--keyring /usr/share/keyrings/ros2-latest-archive-keyring.gpg \
--armor --export > "${KEY_DIR}/ros.asc"
sudo apt-get update -q

# Install packaging tools needed on the runner (bloom for debian/ generation,
# devscripts/dpkg-dev for dpkg-buildpackage, rsync for orig tarball creation)
sudo apt-get install -q -y python3-bloom devscripts dpkg-dev rsync

# Initialise rosdep -- bloom uses it to resolve ROS keys to Debian package names
if [[ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]]; then
sudo rosdep init
fi
rosdep update --ros-distro "${ROS_DISTRO}"

echo "::endgroup::"

# ── Source package generation ─────────────────────────────────────────────────
echo "::group::bloom-generate debian"
cd "${PACKAGE_DIR}"
bloom-generate debian \
--os-name "${OS_NAME}" \
--os-version "${OS_CODE_NAME}" \
--ros-distro "${ROS_DISTRO}"
echo "::endgroup::"

DEB_SOURCE=$(dpkg-parsechangelog --show-field Source)
DEB_VERSION=$(dpkg-parsechangelog --show-field Version)
UPSTREAM_VERSION="${DEB_VERSION%-*}"

# Derive the ROS package name from the Debian source name.
# bloom names sources ros-<distro>-<pkg-with-hyphens>; reverse to get pkg name.
PKG_NAME=$(echo "${DEB_SOURCE}" | sed "s/^ros-${ROS_DISTRO}-//;s/-/_/g")

echo "Debian source: ${DEB_SOURCE}"
echo "Debian version: ${DEB_VERSION}"
echo "Upstream version: ${UPSTREAM_VERSION}"
echo "ROS package name: ${PKG_NAME}"

# Create orig tarball -- dpkg-buildpackage -S needs it in the parent directory
UPSTREAM_DIR="${WORK_DIR}/${DEB_SOURCE}-${UPSTREAM_VERSION}"
mkdir -p "${UPSTREAM_DIR}"
rsync -a --exclude='.git' --exclude='debian' "${PACKAGE_DIR}/" "${UPSTREAM_DIR}/"
tar czf "${WORK_DIR}/${DEB_SOURCE}_${UPSTREAM_VERSION}.orig.tar.gz" \
-C "${WORK_DIR}" "${DEB_SOURCE}-${UPSTREAM_VERSION}"

echo "::group::dpkg-buildpackage -S (source package)"
dpkg-buildpackage -S -us -uc -d
echo "::endgroup::"

# Copy source artifacts to the binarypkg dir that the Docker pipeline will use
find "${WORK_DIR}" -maxdepth 1 \
\( -name "${DEB_SOURCE}_*.dsc" -o -name "${DEB_SOURCE}_*.tar.*" \) \
-exec cp {} "${BINARYPKG_DIR}/" \;

# ── Binary build via buildfarm Docker pipeline ────────────────────────────────
# run_binarydeb_job.py generates the Dockerfile for the "create task" container.
# That container (when run) extracts the source and generates the binary build
# Dockerfile via create_binarydeb_task_generator.py. We then build and run that
# second container to produce the .deb. This mirrors the buildfarm binary job
# exactly, with --skip-download-sourcepkg bypassing the apt source fetch.
RBF_ARGS=(
--rosdistro-index-url "${CONFIG_URL}"
"${ROS_DISTRO}"
"${PKG_NAME}"
"${OS_NAME}"
"${OS_CODE_NAME}"
"${ARCH}"
--distribution-repository-urls "http://packages.ros.org/ros2/${OS_NAME}"
--distribution-repository-key-files "${KEY_DIR}/ros.asc"
--binarypkg-dir "${BINARYPKG_DIR}"
--dockerfile-dir "${DOCKERFILE_CREATE_DIR}"
--skip-download-sourcepkg
)
if [[ "${SKIP_TESTS}" == "true" ]]; then
RBF_ARGS+=(--skip-tests)
fi

echo "::group::Generate binary task Dockerfile"
if [[ $(id -u) -eq 0 ]]; then
# ros_buildfarm scripts assert uid != 0. Running as root (e.g. in act),
# so create a build user and su to it for this call. Root can su without
# a password. The uid is embedded in the generated Dockerfiles.
id rbf &>/dev/null || useradd -m -u 1001 -s /bin/bash rbf
DOCKER_GID=$(stat -c %g /var/run/docker.sock 2>/dev/null || echo 0)
usermod -aG "${DOCKER_GID}" rbf 2>/dev/null || true
chown -R rbf:rbf "${WORK_DIR}"
# shellcheck disable=SC2046
su rbf -s /bin/bash -c "PATH=${PATH} run_binarydeb_job.py $(printf '%q ' "${RBF_ARGS[@]}")"
else
run_binarydeb_job.py "${RBF_ARGS[@]}"
fi
echo "::endgroup::"

IMAGE_TAG="${ROS_DISTRO}_${OS_CODE_NAME}"

echo "::group::Docker build: create-task"
docker build -t "binarydeb_create:${IMAGE_TAG}" "${DOCKERFILE_CREATE_DIR}"
echo "::endgroup::"

echo "::group::Docker run: create-task"
# Runs get_sourcedeb.py --skip-download-sourcepkg (just dpkg-source -x on the .dsc)
# then create_binarydeb_task_generator.py which generates the binary build Dockerfile.
docker run --rm \
-v "${RBF_DIR}:/tmp/ros_buildfarm:ro" \
-v "${BINARYPKG_DIR}:/tmp/binarydeb" \
-v "${DOCKERFILE_BUILD_DIR}:/tmp/docker_build_binarydeb" \
-e DOCKER_PARENT_PID=$$ \
"binarydeb_create:${IMAGE_TAG}"
echo "::endgroup::"

echo "::group::Docker build: binary build"
docker build -t "binarydeb_build:${IMAGE_TAG}" "${DOCKERFILE_BUILD_DIR}"
echo "::endgroup::"

echo "::group::Docker run: binary build (produces .deb)"
# Runs build_binarydeb.py which calls apt-src import + dpkg-buildpackage -b.
# The .deb files land in /tmp/binarydeb (= ${BINARYPKG_DIR} on the host).
docker run --rm \
-v "${RBF_DIR}:/tmp/ros_buildfarm:ro" \
-v "${BINARYPKG_DIR}:/tmp/binarydeb" \
-e DOCKER_PARENT_PID=$$ \
"binarydeb_build:${IMAGE_TAG}"
echo "::endgroup::"

# ── Collect artifacts ─────────────────────────────────────────────────────────
DEB_DIR="${GITHUB_WORKSPACE:-.}/.ros_buildfarm_debs"
mkdir -p "${DEB_DIR}"
find "${BINARYPKG_DIR}" -name '*.deb' -exec cp {} "${DEB_DIR}/" \;

if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "deb_dir=${DEB_DIR}" >> "${GITHUB_OUTPUT}"
fi

echo "Generated packages:"
ls -lh "${DEB_DIR}"
Loading