Skip to content
Merged
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
74 changes: 74 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# UbeROS environment configuration (installer template)
# The installer (install.sh) copies this file to `.env` on first run and never
# overwrites an existing `.env`. Edit `.env` (not this template) to adjust your
# environment; `.env` is gitignored so machine- or company-specific overrides
# are never pushed to the repo. All values below are neutral, safe defaults.

# --- ROS / Gazebo selection ---
# Switching the ROS 2 distribution (e.g. kilted -> jazzy) is a one-line change.
# See ADR-001.
ROS_DISTRO=kilted
GZ_RELEASE=ionic

# --- ROS workspace location ---
# Absolute host directory for the ROS workspace; its `src` subfolder is
# bind-mounted into the ROS and editor services at /ros_ws/src. Keep it OUTSIDE
# the source checkout so workspace builds never dirty the repository (BR-009,
# FR-F1). install.sh fills this in on first run with $HOME/uberos-workspace and
# creates the directory; edit the value to relocate the workspace.
UBEROS_WORKSPACE=

# --- Ingress ---
# Host-published port for the reverse proxy (the only exposed port).
UBEROS_PORT=8080

# --- ROS middleware ---
ROS_DOMAIN_ID=42
RMW_IMPLEMENTATION=rmw_fastrtps_cpp

# --- Software rendering (default; GPU is an opt-in overlay) ---
LIBGL_ALWAYS_SOFTWARE=1
MESA_GL_VERSION_OVERRIDE=3.3
MESA_GLSL_VERSION_OVERRIDE=330

# --- Authentication ---
# Set to "basic" to enable Nginx basic auth at the proxy. Required before any
# non-localhost exposure (NFR N-05). Default is off for localhost Init.
# When "basic", provide credentials first:
# htpasswd -c config/nginx/.htpasswd admin
# The frontend menu shows a Logout action only while auth is enabled (BR-008).
UBEROS_AUTH=off

# --- Operational control plane ---
# Comma-separated services the system menu is allowed to restart (BR-007).
# The control service, proxy, and discovery-server are intentionally excluded.
UBEROS_SERVICES=ros,gazebo,turtlesim,editor,frontend

# Installed simulators exposed by the registry (FR-A1) and which of them
# auto-start at stack up (FR-B8). UBEROS_SIMULATORS is the install set (FR-D):
# list the simulator ids to install and show in the menu; omit one to exclude
# it (its menu entry disappears and launch/stop is rejected). The services are
# always-on (no compose profiles); this gates the registry/menu, not the build.
# Defaults to the whole catalog. UBEROS_SIMULATORS_AUTOSTART, when unset, honors
# each entry's registry `autostart` flag (default both on). The control plane
# starts/stops these on demand and reconciles autostart intent at boot.
UBEROS_SIMULATORS=gazebo,turtlesim
UBEROS_SIMULATORS_AUTOSTART=

# Running UbeROS version surfaced by the control /version endpoint and the
# frontend About dialog (Theme G). The release pipeline overrides it at
# build/deploy time; leave it as `dev` for local development.
UBEROS_VERSION=dev

# --- Intel GPU overlay (compose.override.intel.yaml) ---
# Host-specific group IDs for the DRI render nodes. Find them with
# getent group render video
# and set the numeric GIDs here if the default group names do not resolve.
# UBEROS_RENDER_GID=render
# UBEROS_VIDEO_GID=video

# --- npm registry (frontend build) ---
# Registry used by the frontend image's `npm install`. The default is the public
# npm registry. Point this at a corporate/internal proxy in your local `.env` if
# your network requires it (that value stays out of the repo).
NPM_REGISTRY=https://registry.npmjs.org/
94 changes: 94 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: Release — Build & Push Images
# UbeROS release pipeline (Theme B, FR-B1/FR-B2/FR-B3). On a `v*` version tag,
# build every service image for linux/amd64 and push it to GitHub Container
# Registry (ghcr.io) tagged with the release version. Pre-release tags (a `-`
# in the version, e.g. v0.4.0-beta) additionally publish a moving `:beta` tag so
# testers can track the latest pre-release. The version flows into each image as
# the UBEROS_VERSION build-arg, which stamps the OCI version label (FR-A5, PR-3)
# and the control /version endpoint default.
#
# Bundle assembly, checksums, release notes, and the GitHub Release itself land
# in a later pipeline stage (PR-12); this stage only publishes images.

on:
push:
tags:
- 'v*'

permissions:
contents: read
packages: write

env:
REGISTRY: ghcr.io
IMAGE_NAMESPACE: ghcr.io/jmservera/uberos

jobs:
publish-images:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- service: ros
context: ./services/ros
- service: gazebo
context: ./services/gazebo
- service: turtlesim
context: ./services/turtlesim
- service: editor
context: ./services/editor
- service: frontend
context: ./services/frontend
- service: gzweb-client
context: ./services/gazebo/client
- service: control
context: ./services/control
- service: proxy
context: ./services/proxy
steps:
- uses: actions/checkout@v4

- name: Derive version and image tags
id: meta
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
image="${IMAGE_NAMESPACE}/${{ matrix.service }}"
tags="${image}:${version}"
# A hyphen marks a semver pre-release (e.g. 0.4.0-beta); publish the
# moving :beta tag alongside the pinned version (docs/VERSIONING.md).
case "${version}" in
*-*) tags="$(printf '%s\n%s' "${tags}" "${image}:beta")" ;;
esac
# docker/build-push-action expects one tag per line, so `tags` must be
# emitted as a multiline output (heredoc form), never comma-joined.
{
echo "version=${version}"
echo "tags<<TAGS_EOF"
echo "${tags}"
echo "TAGS_EOF"
} >> "${GITHUB_OUTPUT}"
echo "Publishing ${{ matrix.service }} as:"
echo "${tags}"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push ${{ matrix.service }}
uses: docker/build-push-action@v6
with:
context: ${{ matrix.context }}
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
UBEROS_VERSION=${{ steps.meta.outputs.version }}
NPM_REGISTRY=https://registry.npmjs.org/
177 changes: 177 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/bin/sh
# UbeROS installer (Theme D/E/G, FR-D2/FR-D3/FR-E5/FR-G4).
#
# Bootstraps a UbeROS deployment from either a source checkout (local mode:
# build the images from source) or a released bundle (release mode: pull pinned
# images). This first stage implements local mode and the shared plumbing:
# argument parsing, mode auto-detection, `.env` generation from `.env.template`,
# and `--version`/`--help`. Guided wizard, workspace provisioning, and release
# mode land in later installer stages (PR-7, PR-8, PR-13).
#
# POSIX sh only (no bashisms) so the installer runs on the widest range of hosts.
set -eu

# Installer version (FR-G4). The release pipeline stamps the real version into
# the bundled copy; a source checkout reports `dev`.
INSTALLER_VERSION="dev"

# Resolve the repository/bundle root from this script's location so the
# installer works regardless of the caller's working directory.
SCRIPT_DIR="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P)"
ROOT_DIR="${SCRIPT_DIR}"

ENV_TEMPLATE="${ROOT_DIR}/.env.template"
ENV_FILE="${ROOT_DIR}/.env"

# Default ROS workspace location (FR-F1, BR-009). Deliberately outside the
# source checkout so colcon build/install/log output never dirties the repo. An
# exported UBEROS_WORKSPACE wins; otherwise fall back to $HOME. Without a usable
# HOME there is no safe location to guess, so stay empty and let ensure_env()
# fail with guidance rather than silently polluting the checkout.
DEFAULT_WORKSPACE="${UBEROS_WORKSPACE:-${HOME:+${HOME}/uberos-workspace}}"

MODE="" # empty => auto-detect

log() { printf '%s\n' "$*"; }
err() { printf 'error: %s\n' "$*" >&2; }
die() { err "$*"; exit 1; }

usage() {
cat <<'EOF'
UbeROS installer

Usage: ./install.sh [options]

Options:
--mode <local|release> Installation mode. Defaults to auto-detect:
local = build images from a source checkout
release = use pinned images from a release bundle
--version Print the installer version and exit.
-h, --help Show this help and exit.

Local mode builds every service image from source and starts the stack. Once it
is up, open the UbeROS UI through the proxy (default http://localhost:8080).
EOF
}

# Detect whether this tree is a source checkout (build from source) or a
# released bundle (pinned images). A bundle ships compose.release.yaml; a source
# checkout ships the services/ build contexts.
detect_mode() {
if [ -f "${ROOT_DIR}/compose.release.yaml" ]; then
printf 'release'
elif [ -d "${ROOT_DIR}/services" ] && [ -f "${ROOT_DIR}/compose.yaml" ]; then
printf 'local'
else
printf 'unknown'
fi
}

# Ensure a docker compose CLI is available (v2 plugin or legacy binary).
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose "$@"
else
die "docker compose is required but was not found on PATH"
fi
}

# Create .env from the template on first run; never clobber an existing .env so
# user edits survive re-runs (FR-E5).
ensure_env() {
if [ -f "${ENV_FILE}" ]; then
log "Keeping existing .env (not overwritten)."
return 0
fi
[ -f "${ENV_TEMPLATE}" ] || die "missing ${ENV_TEMPLATE}; cannot generate .env"
[ -n "${DEFAULT_WORKSPACE}" ] || die "cannot determine a workspace location (HOME is unset); set UBEROS_WORKSPACE to an absolute path outside this checkout and re-run"
cp "${ENV_TEMPLATE}" "${ENV_FILE}"
# The template ships UBEROS_WORKSPACE empty on purpose; resolve it to an
# external directory so the source checkout stays clean (FR-F1, BR-009).
sed "s|^UBEROS_WORKSPACE=\$|UBEROS_WORKSPACE=${DEFAULT_WORKSPACE}|" \
"${ENV_FILE}" > "${ENV_FILE}.tmp" && mv "${ENV_FILE}.tmp" "${ENV_FILE}"
log "Generated .env from .env.template (workspace: ${DEFAULT_WORKSPACE})."
Comment on lines +90 to +95
}

# Read a key from .env, ignoring comments and returning the first match.
env_value() {
grep -E "^$1=" "${ENV_FILE}" 2>/dev/null | head -n1 | cut -d= -f2-
}

# Make sure the workspace `src` directory exists before compose bind-mounts it;
# otherwise Docker creates it as root and the container user cannot write.
ensure_workspace() {
workspace="$(env_value UBEROS_WORKSPACE)"
workspace="${workspace:-./workspace}"
# Compose resolves relative bind-mount paths against the project directory
# (ROOT_DIR), not the caller's cwd, so anchor relative values the same way.
case "${workspace}" in
/*) ;;
*) workspace="${ROOT_DIR}/${workspace}" ;;
esac
if [ ! -d "${workspace}/src" ]; then
mkdir -p "${workspace}/src" || die "could not create workspace at ${workspace}/src"
log "Created ROS workspace at ${workspace}"
fi
}

install_local() {
[ -f "${ROOT_DIR}/compose.yaml" ] || die "local mode requires compose.yaml at ${ROOT_DIR}"
ensure_env
ensure_workspace
log "Building UbeROS images from source..."
( cd "${ROOT_DIR}" && compose build )
log "Starting the UbeROS stack..."
( cd "${ROOT_DIR}" && compose up -d )
port="$(env_value UBEROS_PORT)"
port="${port:-8080}"
log "UbeROS is starting. Open the UI at http://localhost:${port}"
}

install_release() {
# Release mode (pinned images, checksum verification, upgrade) is implemented
# in a later installer stage (PR-13). Fail clearly until then.
die "release mode is not available in this installer build yet"
}

# --- Argument parsing (POSIX) ------------------------------------------------
while [ "$#" -gt 0 ]; do
case "$1" in
--mode)
[ "$#" -ge 2 ] || die "--mode requires an argument (local|release)"
MODE="$2"
shift 2
;;
--mode=*)
MODE="${1#--mode=}"
shift
;;
--version)
printf '%s\n' "${INSTALLER_VERSION}"
exit 0
;;
-h|--help)
usage
exit 0
;;
*)
err "unknown option: $1"
usage >&2
exit 2
;;
esac
done

if [ -z "${MODE}" ]; then
MODE="$(detect_mode)"
[ "${MODE}" = "unknown" ] && die "could not auto-detect install mode; pass --mode local|release"
log "Auto-detected install mode: ${MODE}"
fi

case "${MODE}" in
local) install_local ;;
release) install_release ;;
*) die "invalid mode: ${MODE} (expected local or release)" ;;
esac
Loading
Loading