From 665aa75fd871ed028e87adf4bd27adf6e74264ba Mon Sep 17 00:00:00 2001 From: Jacob Bandes-Storch Date: Tue, 12 Jul 2022 12:54:44 -0700 Subject: [PATCH] Add [ROS] version service (#8169) * Add [ROS] version service * review feedback * add spaces --- services/ros/ros-version.service.js | 154 +++++++++++++++++++++++ services/ros/ros-version.service.spec.js | 44 +++++++ services/ros/ros-version.tester.js | 28 +++++ 3 files changed, 226 insertions(+) create mode 100644 services/ros/ros-version.service.js create mode 100644 services/ros/ros-version.service.spec.js create mode 100644 services/ros/ros-version.tester.js diff --git a/services/ros/ros-version.service.js b/services/ros/ros-version.service.js new file mode 100644 index 0000000000000..18e72bbfbf00f --- /dev/null +++ b/services/ros/ros-version.service.js @@ -0,0 +1,154 @@ +import gql from 'graphql-tag' +import Joi from 'joi' +import yaml from 'js-yaml' +import { renderVersionBadge } from '../version.js' +import { GithubAuthV4Service } from '../github/github-auth-service.js' +import { NotFound, InvalidResponse } from '../index.js' + +const tagsSchema = Joi.object({ + data: Joi.object({ + repository: Joi.object({ + refs: Joi.object({ + edges: Joi.array() + .items({ + node: Joi.object({ + name: Joi.string().required(), + }).required(), + }) + .required(), + }).required(), + }).required(), + }).required(), +}).required() + +const contentSchema = Joi.object({ + data: Joi.object({ + repository: Joi.object({ + object: Joi.object({ + text: Joi.string().required(), + }).allow(null), + }).required(), + }).required(), +}).required() + +const distroSchema = Joi.object({ + repositories: Joi.object().required(), +}) +const packageSchema = Joi.object({ + release: Joi.object({ + version: Joi.string().required(), + }).required(), +}) + +export default class RosVersion extends GithubAuthV4Service { + static category = 'version' + + static route = { base: 'ros/v', pattern: ':distro/:packageName' } + + static examples = [ + { + title: 'ROS Package Index', + namedParams: { distro: 'humble', packageName: 'vision_msgs' }, + staticPreview: { + ...renderVersionBadge({ version: '4.0.0' }), + label: 'ros | humble', + }, + }, + ] + + static defaultBadgeData = { label: 'ros' } + + async handle({ distro, packageName }) { + const tagsJson = await this._requestGraphql({ + query: gql` + query ($refPrefix: String!) { + repository(owner: "ros", name: "rosdistro") { + refs( + refPrefix: $refPrefix + first: 30 + orderBy: { field: TAG_COMMIT_DATE, direction: DESC } + ) { + edges { + node { + name + } + } + } + } + } + `, + variables: { refPrefix: `refs/tags/${distro}/` }, + schema: tagsSchema, + }) + + // Filter for tags that look like dates: humble/2022-06-10 + const tags = tagsJson.data.repository.refs.edges + .map(edge => edge.node.name) + .filter(tag => /^\d+-\d+-\d+$/.test(tag)) + .sort() + .reverse() + + const ref = tags[0] ? `refs/tags/${distro}/${tags[0]}` : 'refs/heads/master' + const prettyRef = tags[0] ? `${distro}/${tags[0]}` : 'master' + + const contentJson = await this._requestGraphql({ + query: gql` + query ($expression: String!) { + repository(owner: "ros", name: "rosdistro") { + object(expression: $expression) { + ... on Blob { + text + } + } + } + } + `, + variables: { + expression: `${ref}:${distro}/distribution.yaml`, + }, + schema: contentSchema, + }) + + if (!contentJson.data.repository.object) { + throw new NotFound({ + prettyMessage: `distribution.yaml not found: ${distro}@${prettyRef}`, + }) + } + const version = this.constructor._parseReleaseVersionFromDistro( + contentJson.data.repository.object.text, + packageName + ) + + return { ...renderVersionBadge({ version }), label: `ros | ${distro}` } + } + + static _parseReleaseVersionFromDistro(distroYaml, packageName) { + let distro + try { + distro = yaml.load(distroYaml) + } catch (err) { + throw new InvalidResponse({ + prettyMessage: 'unparseable distribution.yml', + underlyingError: err, + }) + } + + const validatedDistro = this._validate(distro, distroSchema, { + prettyErrorMessage: 'invalid distribution.yml', + }) + if (!validatedDistro.repositories[packageName]) { + throw new NotFound({ prettyMessage: `package not found: ${packageName}` }) + } + + const packageInfo = this._validate( + validatedDistro.repositories[packageName], + packageSchema, + { + prettyErrorMessage: `invalid section for ${packageName} in distribution.yml`, + } + ) + + // Strip off "release inc" suffix + return packageInfo.release.version.replace(/-\d+$/, '') + } +} diff --git a/services/ros/ros-version.service.spec.js b/services/ros/ros-version.service.spec.js new file mode 100644 index 0000000000000..f221198745f28 --- /dev/null +++ b/services/ros/ros-version.service.spec.js @@ -0,0 +1,44 @@ +import { expect } from 'chai' +import RosVersion from './ros-version.service.js' + +describe('parseReleaseVersionFromDistro', function () { + it('returns correct version', function () { + expect( + RosVersion._parseReleaseVersionFromDistro( + ` +%YAML 1.1 +# ROS distribution file +# see REP 143: http://ros.org/reps/rep-0143.html +--- +release_platforms: + debian: + - bullseye + rhel: + - '8' + ubuntu: + - jammy +repositories: + vision_msgs: + doc: + type: git + url: https://github.com/ros-perception/vision_msgs.git + version: ros2 + release: + tags: + release: release/humble/{package}/{version} + url: https://github.com/ros2-gbp/vision_msgs-release.git + version: 4.0.0-2 + source: + test_pull_requests: true + type: git + url: https://github.com/ros-perception/vision_msgs.git + version: ros2 + status: developed +type: distribution +version: 2 + `, + 'vision_msgs' + ) + ).to.equal('4.0.0') + }) +}) diff --git a/services/ros/ros-version.tester.js b/services/ros/ros-version.tester.js new file mode 100644 index 0000000000000..0f3e221736a2e --- /dev/null +++ b/services/ros/ros-version.tester.js @@ -0,0 +1,28 @@ +import { isSemver } from '../test-validators.js' +import { createServiceTester } from '../tester.js' + +export const t = await createServiceTester() + +t.create('gets the package version of vision_msgs in active distro') + .get('/humble/vision_msgs.json') + .expectBadge({ label: 'ros | humble', message: isSemver }) + +t.create('gets the package version of vision_msgs in EOL distro') + .get('/lunar/vision_msgs.json') + .expectBadge({ label: 'ros | lunar', message: isSemver }) + +t.create('returns not found for invalid package') + .get('/humble/this package does not exist - ros test.json') + .expectBadge({ + label: 'ros', + color: 'red', + message: 'package not found: this package does not exist - ros test', + }) + +t.create('returns error for invalid distro') + .get('/xxxxxx/vision_msgs.json') + .expectBadge({ + label: 'ros', + color: 'red', + message: 'distribution.yaml not found: xxxxxx@master', + })