diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fc33672..c6b2adc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,18 +1,59 @@ name: 'Test setup-gazebo' on: - workflow_dispatch: + pull_request: + push: + branches: + - main + - "releases/*" + schedule: + # Run the CI automatically twice per day to look for flakyness. + - cron: "0 */12 * * *" jobs: - test_linux: + test_gazebo_install_ubuntu: + name: 'Check installation of Gazebo on Ubuntu' runs-on: ubuntu-latest container: image: ${{ matrix.docker_image }} strategy: fail-fast: false matrix: - docker_image: - - ubuntu:20.04 - - ubuntu:22.04 + gazebo_distribution: + - citadel + - fortress + - garden + - harmonic + include: + # Gazebo Citadel (Dec 2019 - Dec 2024) + - docker_image: ubuntu:focal + gazebo_distribution: citadel + + # Gazebo Fortress (Sep 2021 - Sep 2026) + - docker_image: ubuntu:focal + gazebo_distribution: fortress + + # Gazebo Garden (Sep 2022 - Nov 2024) + - docker_image: ubuntu:focal + gazebo_distribution: garden + + # Gazebo Harmonic (Sep 2023 - Sep 2028) + - docker_image: ubuntu:jammy + gazebo_distribution: harmonic steps: - - uses: actions/checkout@v2 - - name: 'Linux setup' - uses: ./ \ No newline at end of file + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4.0.2 + with: + node-version: "20.x" + - name: 'Check Gazebo installation on Ubuntu runner' + uses: ./ + with: + required-gazebo-distributions: ${{ matrix.gazebo_distribution }} + - name: 'Test Gazebo installation' + run: | + if command -v ign > /dev/null; then + ign gazebo --versions + elif command -v gz > /dev/null; then + gz sim --versions + else + echo "Neither ign nor gz command found" + exit 1 + fi diff --git a/action.yml b/action.yml index 99c9be3..877158f 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,20 @@ name: 'Setup Gazebo release' description: | Install a Gazebo release on a Linux system +required-gazebo-distributions: + description: | + List of Gazebo distributions to be installed. + + Allowed Gazebo distributions + - citadel + - fortress + - garden + - harmonic + + Multiple values can be passed using a whitespace delimited list + "fortress garden". + required: false + default: '' runs: using: node20 main: dist/index.js diff --git a/dist/index.js b/dist/index.js index 5aaaf6a..402546c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -26189,7 +26189,6 @@ exports.runAptGetInstall = void 0; const utils = __importStar(__nccwpck_require__(1314)); const aptCommandLine = [ "DEBIAN_FRONTEND=noninteractive", - "RTI_NC_LICENSE_ACCEPTED=yes", "apt-get", "install", "--no-install-recommends", @@ -26286,7 +26285,7 @@ function configOs() { yield utils.exec("sudo", ["bash", "-c", "echo 'Etc/UTC' > /etc/timezone"]); yield utils.exec("sudo", ["apt-get", "update"]); // Install tools required to configure the worker system. - yield apt.runAptGetInstall(["curl", "gnupg2", "locales", "lsb-release"]); + yield apt.runAptGetInstall(["wget", "curl", "gnupg2", "locales", "lsb-release", "ca-certificates"]); // Select a locale supporting Unicode. yield utils.exec("sudo", ["locale-gen", "en_US", "en_US.UTF-8"]); core.exportVariable("LANG", "en_US.UTF-8"); @@ -26301,12 +26300,51 @@ function configOs() { yield apt.runAptGetInstall(["tzdata"]); }); } +/** + * Add OSRF APT repository key. + * + * This is necessary even when building from source to install colcon, vcs, etc. + */ +function addAptRepoKey() { + return __awaiter(this, void 0, void 0, function* () { + yield utils.exec("sudo", [ + "bash", + "-c", + `wget https://packages.osrfoundation.org/gazebo.gpg -O \ + /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg`, + ]); + }); +} +/** + * Add OSRF APT repository. + * + * @param ubuntuCodename the Ubuntu version codename + */ +function addAptRepo(ubuntuCodename) { + return __awaiter(this, void 0, void 0, function* () { + yield utils.exec("sudo", [ + "bash", + "-c", + `echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] \ + http://packages.osrfoundation.org/gazebo/ubuntu-stable ${ubuntuCodename} main" | \ + sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null`, + ]); + yield utils.exec("sudo", ["apt-get", "update"]); + }); +} /** * Install Gazebo on a Linux worker. */ function runLinux() { return __awaiter(this, void 0, void 0, function* () { yield configOs(); + yield addAptRepoKey(); + // Add repo according to Ubuntu version + const ubuntuCodename = yield utils.determineDistribCodename(); + yield addAptRepo(ubuntuCodename); + for (const gazeboDistro of utils.getRequiredGazeboDistributions()) { + yield apt.runAptGetInstall([`gz-${gazeboDistro}`]); + } }); } exports.runLinux = runLinux; @@ -26411,7 +26449,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.determineDistribCodename = exports.exec = void 0; +exports.getRequiredGazeboDistributions = exports.validateDistro = exports.determineDistribCodename = exports.exec = void 0; const actions_exec = __importStar(__nccwpck_require__(1514)); const core = __importStar(__nccwpck_require__(2186)); /** @@ -26455,6 +26493,44 @@ function determineDistribCodename() { }); } exports.determineDistribCodename = determineDistribCodename; +// List of valid Gazebo distributions +const validDistro = ["citadel", "fortress", "garden", "harmonic"]; +/** + * Validate all Gazebo input distribution names + * + * @param requiredGazeboDistributionsList + * @returns boolean Validity of Gazebo distribution + */ +function validateDistro(requiredGazeboDistributionsList) { + for (const gazeboDistro of requiredGazeboDistributionsList) { + if (validDistro.indexOf(gazeboDistro) <= -1) { + return false; + } + } + return true; +} +exports.validateDistro = validateDistro; +/** + * Gets the input of the Gazebo distributions to be installed and + * validates them + * + * @returns string[] List of validated Gazebo distributions + */ +function getRequiredGazeboDistributions() { + let requiredGazeboDistributionsList = []; + const requiredGazeboDistributions = core.getInput("required-gazebo-distributions"); + if (requiredGazeboDistributions) { + requiredGazeboDistributionsList = requiredGazeboDistributions.split(RegExp("\\s")); + } + else { + throw new Error("Input cannot be empty."); + } + if (!validateDistro(requiredGazeboDistributionsList)) { + throw new Error("Input has invalid distribution names."); + } + return requiredGazeboDistributionsList; +} +exports.getRequiredGazeboDistributions = getRequiredGazeboDistributions; /***/ }), diff --git a/src/package_manager/apt.ts b/src/package_manager/apt.ts index ea96257..91a3ba1 100644 --- a/src/package_manager/apt.ts +++ b/src/package_manager/apt.ts @@ -1,13 +1,12 @@ import * as utils from "../utils"; const aptCommandLine: string[] = [ - "DEBIAN_FRONTEND=noninteractive", - "RTI_NC_LICENSE_ACCEPTED=yes", - "apt-get", - "install", - "--no-install-recommends", - "--quiet", - "--yes", + "DEBIAN_FRONTEND=noninteractive", + "apt-get", + "install", + "--no-install-recommends", + "--quiet", + "--yes", ]; /** @@ -24,5 +23,5 @@ const aptCommandLine: string[] = [ * @returns Promise exit code */ export async function runAptGetInstall(packages: string[]): Promise { - return utils.exec("sudo", aptCommandLine.concat(packages)); + return utils.exec("sudo", aptCommandLine.concat(packages)); } \ No newline at end of file diff --git a/src/setup-gazebo-linux.ts b/src/setup-gazebo-linux.ts index 6392def..05acb33 100644 --- a/src/setup-gazebo-linux.ts +++ b/src/setup-gazebo-linux.ts @@ -4,55 +4,89 @@ import * as io from "@actions/io"; import * as apt from "./package_manager/apt"; import * as utils from "./utils"; -import * as path from "path"; -import fs from "fs"; - /** * Configure basic OS stuff. */ async function configOs(): Promise { - // When this action runs in a Docker image, sudo may be missing. - // This installs sudo to avoid having to handle both cases (action runs as - // root, action does not run as root) everywhere in the action. - try { - await io.which("sudo", true); - } catch (err) { - await utils.exec("apt-get", ["update"]); - await utils.exec("apt-get", [ - "install", - "--no-install-recommends", - "--quiet", - "--yes", - "sudo", - ]); - } - - await utils.exec("sudo", ["bash", "-c", "echo 'Etc/UTC' > /etc/timezone"]); - await utils.exec("sudo", ["apt-get", "update"]); - - // Install tools required to configure the worker system. - await apt.runAptGetInstall(["curl", "gnupg2", "locales", "lsb-release"]); - - // Select a locale supporting Unicode. - await utils.exec("sudo", ["locale-gen", "en_US", "en_US.UTF-8"]); - core.exportVariable("LANG", "en_US.UTF-8"); - - // Enforce UTC time for consistency. - await utils.exec("sudo", ["bash", "-c", "echo 'Etc/UTC' > /etc/timezone"]); - await utils.exec("sudo", [ - "ln", - "-sf", - "/usr/share/zoneinfo/Etc/UTC", - "/etc/localtime", - ]); - await apt.runAptGetInstall(["tzdata"]); + // When this action runs in a Docker image, sudo may be missing. + // This installs sudo to avoid having to handle both cases (action runs as + // root, action does not run as root) everywhere in the action. + try { + await io.which("sudo", true); + } catch (err) { + await utils.exec("apt-get", ["update"]); + await utils.exec("apt-get", [ + "install", + "--no-install-recommends", + "--quiet", + "--yes", + "sudo", + ]); + } + + await utils.exec("sudo", ["bash", "-c", "echo 'Etc/UTC' > /etc/timezone"]); + await utils.exec("sudo", ["apt-get", "update"]); + + // Install tools required to configure the worker system. + await apt.runAptGetInstall(["wget", "curl", "gnupg2", "locales", "lsb-release", "ca-certificates"]); + + // Select a locale supporting Unicode. + await utils.exec("sudo", ["locale-gen", "en_US", "en_US.UTF-8"]); + core.exportVariable("LANG", "en_US.UTF-8"); + + // Enforce UTC time for consistency. + await utils.exec("sudo", ["bash", "-c", "echo 'Etc/UTC' > /etc/timezone"]); + await utils.exec("sudo", [ + "ln", + "-sf", + "/usr/share/zoneinfo/Etc/UTC", + "/etc/localtime", + ]); + await apt.runAptGetInstall(["tzdata"]); +} + +/** + * Add OSRF APT repository key. + * + * This is necessary even when building from source to install colcon, vcs, etc. + */ +async function addAptRepoKey(): Promise { + await utils.exec("sudo", [ + "bash", + "-c", + `wget https://packages.osrfoundation.org/gazebo.gpg -O \ + /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg`, + ]); +} + +/** + * Add OSRF APT repository. + * + * @param ubuntuCodename the Ubuntu version codename + */ +async function addAptRepo(ubuntuCodename: string): Promise { + await utils.exec("sudo", [ + "bash", + "-c", + `echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] \ + http://packages.osrfoundation.org/gazebo/ubuntu-stable ${ubuntuCodename} main" | \ + sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null`, + ]); + await utils.exec("sudo", ["apt-get", "update"]); } /** * Install Gazebo on a Linux worker. */ export async function runLinux(): Promise { + await configOs(); + await addAptRepoKey(); - await configOs(); + // Add repo according to Ubuntu version + const ubuntuCodename = await utils.determineDistribCodename(); + await addAptRepo(ubuntuCodename); + for (const gazeboDistro of utils.getRequiredGazeboDistributions()) { + await apt.runAptGetInstall([`gz-${gazeboDistro}`]); + } } diff --git a/src/setup-gazebo.ts b/src/setup-gazebo.ts index 361d43f..d4d00a7 100644 --- a/src/setup-gazebo.ts +++ b/src/setup-gazebo.ts @@ -7,10 +7,10 @@ async function run() { } catch (error) { let errorMessage = "Unknown error"; - if (error instanceof Error) { - errorMessage = error.message; - } - core.setFailed(errorMessage); + if (error instanceof Error) { + errorMessage = error.message; + } + core.setFailed(errorMessage); } } diff --git a/src/utils.ts b/src/utils.ts index e99a37d..234449a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,16 +12,16 @@ import * as im from "@actions/exec/lib/interfaces"; * @returns Promise exit code */ export async function exec( - commandLine: string, - args?: string[], - options?: im.ExecOptions, - log_message?: string, + commandLine: string, + args?: string[], + options?: im.ExecOptions, + log_message?: string, ): Promise { - const argsAsString = (args || []).join(" "); - const message = log_message || `Invoking "${commandLine} ${argsAsString}"`; - return core.group(message, () => { - return actions_exec.exec(commandLine, args, options); - }); + const argsAsString = (args || []).join(" "); + const message = log_message || `Invoking "${commandLine} ${argsAsString}"`; + return core.group(message, () => { + return actions_exec.exec(commandLine, args, options); + }); } /** @@ -33,17 +33,59 @@ export async function exec( * @returns Promise Ubuntu distribution codename (e.g. "focal") */ export async function determineDistribCodename(): Promise { - let distribCodename = ""; - const options: im.ExecOptions = {}; - options.listeners = { - stdout: (data: Buffer) => { - distribCodename += data.toString(); - }, - }; - await exec( - "bash", - ["-c", 'source /etc/lsb-release ; echo -n "$DISTRIB_CODENAME"'], - options, - ); - return distribCodename; + let distribCodename = ""; + const options: im.ExecOptions = {}; + options.listeners = { + stdout: (data: Buffer) => { + distribCodename += data.toString(); + }, + }; + await exec( + "bash", + ["-c", 'source /etc/lsb-release ; echo -n "$DISTRIB_CODENAME"'], + options, + ); + return distribCodename; +} + +// List of valid Gazebo distributions +const validDistro: string[] = ["citadel", "fortress", "garden", "harmonic"]; + +/** + * Validate all Gazebo input distribution names + * + * @param requiredGazeboDistributionsList + * @returns boolean Validity of Gazebo distribution + */ +export function validateDistro( + requiredGazeboDistributionsList: string[], +): boolean { + for (const gazeboDistro of requiredGazeboDistributionsList) { + if (validDistro.indexOf(gazeboDistro) <= -1) { + return false; + } + } + return true; +} + +/** + * Gets the input of the Gazebo distributions to be installed and + * validates them + * + * @returns string[] List of validated Gazebo distributions + */ +export function getRequiredGazeboDistributions(): string[] { + let requiredGazeboDistributionsList: string[] = []; + const requiredGazeboDistributions = core.getInput("required-gazebo-distributions"); + if (requiredGazeboDistributions) { + requiredGazeboDistributionsList = requiredGazeboDistributions.split( + RegExp("\\s"), + ); + } else { + throw new Error("Input cannot be empty."); + } + if (!validateDistro(requiredGazeboDistributionsList)) { + throw new Error("Input has invalid distribution names."); + } + return requiredGazeboDistributionsList; } \ No newline at end of file