Skip to content

Feature/eincinx

Feature/eincinx #17

name: Build Common Deb
# Runs in https://github.com/fiveages-sim/robot-descriptions-common.
# The repository root is the colcon workspace.
#
# Release policy:
# - push v* tag -> formal GitHub Release
# - pull_request merged into main -> single rolling pre-release (tag: pre-release)
# deb Version: patch bump on latest formal; reset when pre-release X.Y drifts
# - workflow_dispatch -> manual formal release for a given tag
on:
push:
tags:
- "v*"
pull_request:
types:
- closed
branches:
- main
workflow_dispatch:
inputs:
tag:
description: Release tag to upload the deb asset to
required: false
type: string
ros_distro:
description: ROS distro used in the install path
required: false
default: jazzy
type: string
permissions:
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref_type == 'tag' && github.ref_name || (github.event_name == 'pull_request' && 'pre-release' || github.ref) }}
cancel-in-progress: true
env:
DEFAULT_ROS_DISTRO: jazzy
PACKAGE_NAME: ros-jazzy-robot-descriptions-common
DEB_FILE_PREFIX: ros-jazzy-robot-descriptions-common
INSTALL_PREFIX: /opt/ros/jazzy
PRE_RELEASE_TAG: pre-release
jobs:
build_common_deb:
name: build common deb (${{ matrix.arch }})
if: github.event_name != 'pull_request' || github.event.pull_request.merged == true
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
ros_distro: jazzy
image: ros:jazzy-ros-base-noble
- arch: arm64
runner: ubuntu-24.04-arm
ros_distro: jazzy
image: ros:jazzy-ros-base-noble
container:
image: ${{ matrix.image }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.merge_commit_sha || github.ref }}
- name: Resolve release metadata
id: meta
shell: bash
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
INPUT_ROS_DISTRO: ${{ github.event.inputs.ros_distro }}
MATRIX_ROS_DISTRO: ${{ matrix.ros_distro }}
DEB_ARCH: ${{ matrix.arch }}
PRE_RELEASE_TAG: ${{ env.PRE_RELEASE_TAG }}
MERGE_COMMIT_SHA: ${{ github.event.pull_request.merge_commit_sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ github.token }}
run: |
set -eo pipefail
resolve_prerelease_deb_version() {
local short_sha="$1"
local release_repo="$2"
local fallback_package_xml="$3"
RELEASE_REPO="${release_repo}" \
SHORT_SHA="${short_sha}" \
FALLBACK_PACKAGE_XML="${fallback_package_xml}" \
PRE_RELEASE_TAG="${PRE_RELEASE_TAG}" \
GH_TOKEN="${GH_TOKEN:-}" \
python3 "${GITHUB_WORKSPACE}/.github/scripts/resolve_prerelease_deb_version.py"
}
is_prerelease="false"
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
if [[ -z "${MERGE_COMMIT_SHA}" ]]; then
echo "Merge commit SHA is missing for merged pull request." >&2
exit 1
fi
short_sha="$(printf '%s' "${MERGE_COMMIT_SHA}" | cut -c1-7)"
tag="${PRE_RELEASE_TAG}"
version="$(resolve_prerelease_deb_version "${short_sha}" "${GITHUB_REPOSITORY}" "robot_common_launch/package.xml")"
is_prerelease="true"
elif [[ -n "${INPUT_TAG:-}" ]]; then
tag="$INPUT_TAG"
elif [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
tag="${GITHUB_REF_NAME}"
else
echo "A release tag is required for workflow_dispatch." >&2
exit 1
fi
if [[ "${is_prerelease}" != "true" ]]; then
if [[ "${tag}" != [vV]* ]]; then
tag="v${tag}"
fi
version="${tag#v}"
version="${version#V}"
version="$(printf '%s' "$version" | sed 's/-/~/g')"
fi
if [[ -z "$version" ]]; then
echo "Unable to derive a package version from tag: $tag" >&2
exit 1
fi
ros_distro="${INPUT_ROS_DISTRO:-$MATRIX_ROS_DISTRO}"
if [[ "${ros_distro}" != "${MATRIX_ROS_DISTRO}" ]]; then
echo "This containerized deb workflow currently supports only ROS 2 ${MATRIX_ROS_DISTRO}." >&2
echo "Requested: ${ros_distro}" >&2
exit 1
fi
# GitHub Release rewrites "~" to "." in uploaded asset filenames.
deb_file_version="${version//~/.}"
deb_file="${DEB_FILE_PREFIX}_${deb_file_version}_${DEB_ARCH}.deb"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "ros_distro=$ros_distro" >> "$GITHUB_OUTPUT"
echo "deb_arch=$DEB_ARCH" >> "$GITHUB_OUTPUT"
echo "deb_file=$deb_file" >> "$GITHUB_OUTPUT"
echo "is_prerelease=$is_prerelease" >> "$GITHUB_OUTPUT"
- name: Validate workspace
shell: bash
run: |
set -eo pipefail
if [[ ! -f robot_common_launch/package.xml ]]; then
echo "robot_common_launch/package.xml not found at repo root." >&2
exit 1
fi
- name: Install build tools
shell: bash
run: |
set -eo pipefail
apt-get update
apt-get install -y \
python3-rosdep \
python3-colcon-core \
python3-colcon-common-extensions \
python3-vcstool \
python3-pip \
python3-catkin-pkg-modules \
sudo \
zstd \
fakeroot \
dpkg-dev \
rsync \
build-essential \
cmake \
ros-${{ steps.meta.outputs.ros_distro }}-ament-cmake \
ros-${{ steps.meta.outputs.ros_distro }}-ament-cmake-core \
ros-${{ steps.meta.outputs.ros_distro }}-ament-package
source "/opt/ros/${{ steps.meta.outputs.ros_distro }}/setup.bash"
python3 -c "import ament_package; print('ament_package ready:', ament_package.__file__)"
- name: Prepare rosdep user
shell: bash
run: |
set -eo pipefail
if ! id -u rosdep >/dev/null 2>&1; then
useradd --create-home --shell /bin/bash rosdep
fi
mkdir -p /home/rosdep/.ros/rosdep
chown -R rosdep:rosdep /home/rosdep/.ros
echo "rosdep ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/rosdep
chmod 0440 /etc/sudoers.d/rosdep
- name: Cache rosdep sources
id: cache-rosdep
uses: actions/cache@v5
with:
path: /home/rosdep/.ros/rosdep/sources.cache
key: rosdep-${{ runner.os }}-${{ matrix.arch }}-${{ steps.meta.outputs.ros_distro }}-${{ hashFiles('**/package.xml') }}
restore-keys: |
rosdep-${{ runner.os }}-${{ matrix.arch }}-${{ steps.meta.outputs.ros_distro }}-
rosdep-${{ runner.os }}-${{ matrix.arch }}-
- name: Update rosdep sources
shell: bash
run: |
set -eo pipefail
chown -R rosdep:rosdep /home/rosdep/.ros
if [[ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]]; then
PYTHONWARNINGS="ignore:pkg_resources is deprecated as an API:DeprecationWarning" rosdep init
fi
if [[ "${{ steps.cache-rosdep.outputs.cache-hit }}" == "true" ]]; then
echo "Using restored rosdep sources cache."
else
sudo -H -u rosdep env \
PYTHONWARNINGS="ignore:pkg_resources is deprecated as an API:DeprecationWarning" \
rosdep update
fi
- name: Install common ROS dependencies
shell: bash
env:
ROS_DISTRO: ${{ steps.meta.outputs.ros_distro }}
run: |
set -eo pipefail
source "/opt/ros/${ROS_DISTRO}/setup.bash"
ros_python_path="/opt/ros/${ROS_DISTRO}/lib/python$(python3 -c 'import sys; print(str(sys.version_info.major) + "." + str(sys.version_info.minor))')/site-packages"
sudo -H -u rosdep env \
GITHUB_WORKSPACE="${GITHUB_WORKSPACE}" \
PYTHONPATH="${ros_python_path}:${PYTHONPATH:-}" \
PYTHONWARNINGS="ignore:pkg_resources is deprecated as an API:DeprecationWarning" \
ROS_DISTRO="${ROS_DISTRO}" \
bash -lc '
source "/opt/ros/${ROS_DISTRO}/setup.bash"
rosdep install \
--from-paths "${GITHUB_WORKSPACE}" \
--ignore-src \
--rosdistro "${ROS_DISTRO}" \
-r \
-y
'
- name: Build common install space
shell: bash
env:
ROS_DISTRO: ${{ steps.meta.outputs.ros_distro }}
run: |
set -eo pipefail
source "/opt/ros/${ROS_DISTRO}/setup.bash"
ros_python_path="/opt/ros/${ROS_DISTRO}/lib/python$(python3 -c 'import sys; print(str(sys.version_info.major) + "." + str(sys.version_info.minor))')/site-packages"
export PYTHONPATH="${ros_python_path}:${PYTHONPATH:-}"
build_root="${RUNNER_TEMP}/colcon-common"
package_root="${RUNNER_TEMP}/deb-build/package"
install_prefix="${INSTALL_PREFIX}"
install_root="${package_root}${install_prefix}"
rm -rf "$build_root" "$package_root"
mkdir -p "$build_root" "$package_root"
colcon --log-base "${build_root}/log" build \
--base-paths "${GITHUB_WORKSPACE}" \
--build-base "${build_root}/build" \
--install-base "${install_root}" \
--merge-install \
--cmake-args \
--no-warn-unused-cli \
-DBUILD_TESTING=OFF \
-DCMAKE_BUILD_TYPE=Release
python3 -c "import sys; exec('''import os\ninstall_root = sys.argv[1]\nruntime_prefix = sys.argv[2]\nfor dirpath, _, filenames in os.walk(install_root):\n for filename in filenames:\n path = os.path.join(dirpath, filename)\n try:\n with open(path, 'r', encoding='utf-8') as f:\n content = f.read()\n except Exception:\n continue\n if install_root not in content:\n continue\n with open(path, 'w', encoding='utf-8') as f:\n f.write(content.replace(install_root, runtime_prefix))''')" "$install_root" "$install_prefix"
rm -f \
"${install_root}/setup.bash" \
"${install_root}/setup.sh" \
"${install_root}/setup.zsh" \
"${install_root}/setup.ps1" \
"${install_root}/local_setup.bash" \
"${install_root}/local_setup.sh" \
"${install_root}/local_setup.zsh" \
"${install_root}/local_setup.ps1" \
"${install_root}/_local_setup_util.py" \
"${install_root}/_local_setup_util_sh.py" \
"${install_root}/_local_setup_util_ps1.py" \
"${install_root}/.colcon_install_layout" \
"${install_root}/COLCON_IGNORE"
if [[ ! -d "${install_root}/share/ament_index/resource_index/packages" ]]; then
echo "ament index was not generated in the install space." >&2
exit 1
fi
for required_pkg in robot_common_launch sensor_models component_models jodell_description; do
if [[ ! -d "${install_root}/share/${required_pkg}" ]]; then
echo "${required_pkg} was not installed into the package root." >&2
exit 1
fi
done
- name: Build deb package
id: build
shell: bash
env:
ROS_DISTRO: ${{ steps.meta.outputs.ros_distro }}
VERSION: ${{ steps.meta.outputs.version }}
DEB_FILE: ${{ steps.meta.outputs.deb_file }}
DEB_ARCH: ${{ steps.meta.outputs.deb_arch }}
run: |
set -eo pipefail
package_root="${RUNNER_TEMP}/deb-build/package"
install_prefix="${INSTALL_PREFIX}"
mkdir -p "$package_root/DEBIAN"
installed_size="$(du -sk "$package_root" | cut -f1)"
cat > "$package_root/DEBIAN/control" <<EOF
Package: ${PACKAGE_NAME}
Version: ${VERSION}
Section: misc
Priority: optional
Architecture: ${DEB_ARCH}
Depends: ros-${ROS_DISTRO}-ros-base, ros-${ROS_DISTRO}-ament-index-python, ros-${ROS_DISTRO}-controller-manager, ros-${ROS_DISTRO}-joint-state-publisher-gui, ros-${ROS_DISTRO}-launch, ros-${ROS_DISTRO}-launch-ros, ros-${ROS_DISTRO}-rclpy, ros-${ROS_DISTRO}-robot-state-publisher, ros-${ROS_DISTRO}-xacro
Provides: robot-descriptions-jazzy-common
Maintainer: ${GITHUB_REPOSITORY} maintainers <github-actions[bot]@users.noreply.github.com>
Installed-Size: ${installed_size}
Description: Shared robot_descriptions common ROS packages
Prebuilt packages from fiveages-sim/robot-descriptions-common installed into ${INSTALL_PREFIX} while excluding shared prefix setup files.
EOF
if [[ ! -d "${package_root}${install_prefix}/share/robot_common_launch" ]]; then
echo "robot_common_launch was not installed into the package root." >&2
exit 1
fi
dpkg-deb --root-owner-group --build "$package_root" "$DEB_FILE"
echo "deb_path=${GITHUB_WORKSPACE}/${DEB_FILE}" >> "$GITHUB_OUTPUT"
- name: Upload workflow artifact
uses: actions/upload-artifact@v6
with:
name: robot-descriptions-common-${{ steps.meta.outputs.version }}-${{ matrix.arch }}
path: ${{ steps.meta.outputs.deb_file }}
if-no-files-found: error
retention-days: 7
publish_common_deb:
name: publish common debs
runs-on: ubuntu-24.04
needs: build_common_deb
outputs:
tag: ${{ steps.meta.outputs.tag }}
ros_distro: ${{ steps.meta.outputs.ros_distro }}
version: ${{ steps.meta.outputs.version }}
is_prerelease: ${{ steps.meta.outputs.is_prerelease }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.merge_commit_sha || github.ref }}
- name: Resolve release metadata
id: meta
shell: bash
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
INPUT_ROS_DISTRO: ${{ github.event.inputs.ros_distro }}
PRE_RELEASE_TAG: ${{ env.PRE_RELEASE_TAG }}
MERGE_COMMIT_SHA: ${{ github.event.pull_request.merge_commit_sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ github.token }}
run: |
set -eo pipefail
resolve_prerelease_deb_version() {
local short_sha="$1"
local release_repo="$2"
local fallback_package_xml="$3"
RELEASE_REPO="${release_repo}" \
SHORT_SHA="${short_sha}" \
FALLBACK_PACKAGE_XML="${fallback_package_xml}" \
PRE_RELEASE_TAG="${PRE_RELEASE_TAG}" \
GH_TOKEN="${GH_TOKEN:-}" \
python3 "${GITHUB_WORKSPACE}/.github/scripts/resolve_prerelease_deb_version.py"
}
is_prerelease="false"
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
if [[ -z "${MERGE_COMMIT_SHA}" ]]; then
echo "Merge commit SHA is missing for merged pull request." >&2
exit 1
fi
short_sha="$(printf '%s' "${MERGE_COMMIT_SHA}" | cut -c1-7)"
tag="${PRE_RELEASE_TAG}"
version="$(resolve_prerelease_deb_version "${short_sha}" "${GITHUB_REPOSITORY}" "robot_common_launch/package.xml")"
is_prerelease="true"
elif [[ -n "${INPUT_TAG:-}" ]]; then
tag="$INPUT_TAG"
elif [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
tag="${GITHUB_REF_NAME}"
else
echo "A release tag is required for workflow_dispatch." >&2
exit 1
fi
ros_distro="${INPUT_ROS_DISTRO:-$DEFAULT_ROS_DISTRO}"
if [[ "${is_prerelease}" != "true" ]]; then
if [[ "${tag}" != [vV]* ]]; then
tag="v${tag}"
fi
version="${tag#v}"
version="${version#V}"
version="$(printf '%s' "$version" | sed 's/-/~/g')"
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "ros_distro=$ros_distro" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "is_prerelease=$is_prerelease" >> "$GITHUB_OUTPUT"
- name: Download built deb artifacts
uses: actions/download-artifact@v7
with:
pattern: robot-descriptions-common-${{ steps.meta.outputs.version }}-*
merge-multiple: true
- name: Publish debs to GitHub Release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.meta.outputs.tag }}
IS_PRERELEASE: ${{ steps.meta.outputs.is_prerelease }}
PRE_RELEASE_TAG: ${{ env.PRE_RELEASE_TAG }}
RELEASE_TARGET_SHA: ${{ github.event.pull_request.merge_commit_sha || github.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -eo pipefail
ls -lh *.deb
if [[ "${IS_PRERELEASE}" == "true" ]]; then
echo "Cleaning up legacy pre-releases..."
gh release list --repo "${GITHUB_REPOSITORY}" --limit 100 \
--json tagName,isPrerelease \
--jq ".[] | select(.isPrerelease and .tagName != \"${PRE_RELEASE_TAG}\") | .tagName" \
| while read -r old_tag; do
[[ -z "$old_tag" ]] && continue
echo "Deleting legacy pre-release: ${old_tag}"
gh release delete "$old_tag" --repo "${GITHUB_REPOSITORY}" --yes --cleanup-tag || true
done
gh release delete "${TAG}" --repo "${GITHUB_REPOSITORY}" --yes --cleanup-tag 2>/dev/null || true
gh release create "${TAG}" \
--repo "${GITHUB_REPOSITORY}" \
--target "${RELEASE_TARGET_SHA}" \
--title "Pre-release (main)" \
--notes "Rolling pre-release built from merged PR #${PR_NUMBER} @ ${RELEASE_TARGET_SHA}
Workflow run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
--prerelease \
./*.deb
else
if ! gh release view "$TAG" --repo "${GITHUB_REPOSITORY}" > /dev/null 2>&1; then
gh release create "$TAG" \
--repo "${GITHUB_REPOSITORY}" \
--title "$TAG" \
--notes "Automated release for Debian packages."
fi
gh release upload "$TAG" ./*.deb --repo "${GITHUB_REPOSITORY}" --clobber
fi
verify_common_deb:
name: verify common deb (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
needs: publish_common_deb
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
image: ros:jazzy-ros-base-noble
- arch: arm64
runner: ubuntu-24.04-arm
image: ros:jazzy-ros-base-noble
container:
image: ${{ matrix.image }}
steps:
- name: Resolve verify metadata
id: meta
shell: bash
env:
TAG: ${{ needs.publish_common_deb.outputs.tag }}
VERSION: ${{ needs.publish_common_deb.outputs.version }}
ROS_DISTRO: ${{ needs.publish_common_deb.outputs.ros_distro }}
DEB_ARCH: ${{ matrix.arch }}
run: |
set -eo pipefail
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "ros_distro=${ROS_DISTRO}" >> "$GITHUB_OUTPUT"
deb_file_version="${VERSION//~/.}"
echo "deb_file=${DEB_FILE_PREFIX}_${deb_file_version}_${DEB_ARCH}.deb" >> "$GITHUB_OUTPUT"
- name: Install verification tools
shell: bash
run: |
set -eo pipefail
apt-get update
apt-get install -y ca-certificates curl cmake build-essential
mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /etc/apt/keyrings/githubcli-archive-keyring.gpg
chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list
apt-get update
apt-get install -y gh
- name: Download deb from release
id: download-deb
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -eo pipefail
deb_arch="${{ matrix.arch }}"
tag="${{ steps.meta.outputs.tag }}"
deb_file=""
for attempt in {1..30}; do
rm -f ./*.deb 2>/dev/null || true
if ! gh release view "$tag" --repo "${GITHUB_REPOSITORY}" > /dev/null 2>&1; then
echo "Release ${tag} is not available yet (attempt ${attempt}/30); retrying in 20s..."
sleep 20
continue
fi
gh release download "$tag" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "*_${deb_arch}.deb"
deb_file="$(ls -1 *_"${deb_arch}.deb" 2>/dev/null | head -n1 || true)"
if [[ -n "$deb_file" && -f "$deb_file" ]]; then
echo "deb_file=${deb_file}" >> "${GITHUB_OUTPUT}"
break
fi
echo "Asset *_${deb_arch}.deb is not available yet (attempt ${attempt}/30); retrying in 20s..."
gh release view "$tag" \
--repo "${GITHUB_REPOSITORY}" \
--json assets \
--jq '.assets[].name' || true
sleep 20
done
if [[ -z "$deb_file" || ! -f "$deb_file" ]]; then
echo "Timed out waiting for *_${deb_arch}.deb in release ${tag}." >&2
gh release view "$tag" \
--repo "${GITHUB_REPOSITORY}" \
--json assets \
--jq '.assets[].name' || true
exit 1
fi
ls -lh "${deb_file}"
- name: Install deb and verify package discovery
shell: bash
run: |
set -eo pipefail
deb_file="${{ steps.download-deb.outputs.deb_file }}"
ros_distro="${{ steps.meta.outputs.ros_distro }}"
dpkg -i "./${deb_file}" || apt-get -f install -y
set +u
source "/opt/ros/${ros_distro}/setup.bash"
set -u
export CMAKE_PREFIX_PATH="${INSTALL_PREFIX}:${CMAKE_PREFIX_PATH:-}"
export AMENT_PREFIX_PATH="${INSTALL_PREFIX}:${AMENT_PREFIX_PATH:-}"
python3 -c 'import textwrap; exec(textwrap.dedent("""
from ament_index_python.packages import get_package_share_directory
for package_name in [
"robot_common_launch",
"sensor_models",
"component_models",
"jodell_description",
]:
print(package_name, get_package_share_directory(package_name))
import robot_common_launch
print("robot_common_launch module:", robot_common_launch.__file__)
"""))'
rm -rf downstream_check
mkdir -p downstream_check
cat > downstream_check/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.16)
project(robot_descriptions_common_downstream_check)
find_package(sensor_models REQUIRED)
find_package(component_models REQUIRED)
find_package(jodell_description REQUIRED)
EOF
cmake -Wno-dev -S downstream_check -B downstream_check/build