diff --git a/README.md b/README.md
index 3b1f7e3..c3e75a8 100644
--- a/README.md
+++ b/README.md
@@ -54,20 +54,46 @@ LABEL authors='[\
"email": "doe@john.com"\
}\
]'
-LABEL docs='http://path.to.your.docs.com'
LABEL company='{\
- "about": "",\
+ "about": "brief description",\
+ "name": "Company/Person Name",\
+ "email": "email@company.com"\
+}'
+LABEL readme="https://raw.githubusercontent.com/username/repo/{tag}/README.md"
+LABEL links='{\
+ "website": "https://...",\
+ "support": "mailto:support@company.com",\
+ "documentation": "https://docs.company.com/cool-extension/",\
+}'
+LABEL type="example"
+LABEL tags='[\
+ "positioning",\
+ "navigation"\
+]'
```
- `version` is the name of the current tag, which we expect to be a valid [semver](https://semver.org/).
- `permissions`is a json file that follows the [Docker API payload for creating containers](https://docs.docker.com/engine/api/v1.41/#tag/Container/operation/ContainerCreate).
- - `docs` is a url for the documentation of your extension.
- `authors` is a json list of authors of your extension
- - `company` is a json, which currently only contains an "about" section for a brief description about your company.
+ - `company` is a json with information about the maintainer responsible for providing new versions
+ - `readme` is a URL to a markdown-based README file for the extension
+ - `links` is a collection of additional useful/relevant links
+ - `type` is a primary categorisation of the extension, and should be one of:
+ - "device-integration"
+ - "example"
+ - "theme"
+ - "other"
+- `tags` is a collection of relevant tags for filtering, which should be lowercase alpha-numeric with dashes
+ - limit of 10 per extension
+
+Some additional information and examples are available in the
+[metadata documentation](https://docs.bluerobotics.com/ardusub-zola/software/onboard/BlueOS-latest/extensions#metadata-dockerfile).
## How this repo works
- Every time this repo changes, a Github Action runs and goes through all the .json files in here. For each of them, it reaches out to dockerhub and fetches all the available tags, extracting the metadata in LABELS and crafting a complete `manifest.json`, which is stored in this repo's gh-pages branch.
+ Every time this repo changes, a Github Action runs and goes through all the .json files in here. For each of them, it reaches out to dockerhub and fetches all the available tags, extracting the metadata in LABELS and crafting a complete `manifest.json`, which is stored in this repo's `gh-pages` branch.
+
+There is also a [website](https://docs.bluerobotics.com/BlueOS-Extensions-Repository) that gets generated, to show which extensions are currently available in the store.
## Testing the website locally
diff --git a/blueos_repository/consolidate.py b/blueos_repository/consolidate.py
index f0c107c..d0c8495 100755
--- a/blueos_repository/consolidate.py
+++ b/blueos_repository/consolidate.py
@@ -2,6 +2,7 @@
import asyncio
import dataclasses
import json
+from enum import Enum
from pathlib import Path
from typing import Any, AsyncIterable, Dict, List, Optional, Union
@@ -9,7 +10,14 @@
import semver
from registry import Registry
-REPO_ROOT = "https://raw.githubusercontent.com/bluerobotics/BlueOS-Extensions-Repository/master/"
+REPO_ROOT = "https://raw.githubusercontent.com/bluerobotics/BlueOS-Extensions-Repository/master/repos"
+
+
+class StrEnum(str, Enum):
+ """Temporary filler until Python 3.11 available."""
+
+ def __str__(self) -> str:
+ return self.value # type: ignore
class EnhancedJSONEncoder(json.JSONEncoder):
@@ -46,6 +54,13 @@ def from_json(json_dict: Dict[str, str]) -> "Company":
return Company(name=json_dict["name"], email=json_dict.get("email", None), about=json_dict.get("about", None))
+class ExtensionType(StrEnum):
+ DEVICE_INTEGRATION = "device-integration"
+ EXAMPLE = "example"
+ THEME = "theme"
+ OTHER = "other"
+
+
# pylint: disable=too-many-instance-attributes
@dataclasses.dataclass
class Version:
@@ -58,6 +73,14 @@ class Version:
readme: Optional[str]
company: Optional[Company]
support: Optional[str]
+ type: ExtensionType
+ filter_tags: List[str]
+ extra_links: Dict[str, str]
+
+ @staticmethod
+ def validate_filter_tags(tags: List[str]) -> List[str]:
+ """Returns a list of up to 10 lower-case alpha-numeric tags (dashes allowed)."""
+ return [tag.lower() for tag in tags if tag.replace("-", "").isalnum()][:10]
@dataclasses.dataclass
@@ -126,50 +149,65 @@ async def all_repositories(self) -> AsyncIterable[RepositoryEntry]:
raise Exception(f"unable to read file {repo}: {error}") from error
@staticmethod
- def is_valid_semver(string: str) -> bool:
+ def valid_semver(string: str) -> Optional[semver.VersionInfo]:
# We want to allow versions to be prefixed with a 'v'.
# This is up for discussion
if string.startswith("v"):
string = string[1:]
try:
- semver.VersionInfo.parse(string)
- return True
+ return semver.VersionInfo.parse(string)
except ValueError:
- return False
+ return None # not valid
+ # pylint: disable=too-many-locals
async def run(self) -> None:
async for repository in self.all_repositories():
for tag in await self.registry.fetch_remote_tags(repository.docker):
try:
- if not self.is_valid_semver(tag):
+ if not self.valid_semver(tag):
print(f"{tag} is not valid SemVer, ignoring it...")
continue
raw_labels = await self.registry.fetch_labels(f"{repository.docker}:{tag}")
permissions = raw_labels.get("permissions", None)
- website = raw_labels.get("website", None)
+ links = raw_labels.get("links", {})
+ website = links.pop("website", raw_labels.get("website", None))
authors = raw_labels.get("authors", None)
- docs = raw_labels.get("docs", None)
+ # documentation is just a URL for a link, but the old format had it as its own label
+ docs = links.pop("docs", links.pop("documentation", raw_labels.get("docs", None)))
readme = raw_labels.get("readme", None)
if readme is not None:
readme = readme.replace(r"{tag}", tag)
+ try:
+ readme = await self.fetch_readme(readme)
+ except Exception as error: # pylint: disable=broad-except
+ readme = str(error)
company_raw = raw_labels.get("company", None)
company = Company.from_json(json.loads(company_raw)) if company_raw is not None else None
- support = raw_labels.get("support", None)
+ support = links.pop("support", raw_labels.get("support", None))
+ type_ = raw_labels.get("type", ExtensionType.OTHER)
+ filter_tags = raw_labels.get("tags", [])
new_version = Version(
permissions=json.loads(permissions) if permissions else None,
website=website,
authors=json.loads(authors) if authors else [],
docs=json.loads(docs) if docs else None,
- readme=await self.fetch_readme(readme) if readme is not None else None,
+ readme=readme,
company=company,
support=support,
+ extra_links=links,
+ type=type_,
+ filter_tags=Version.validate_filter_tags(filter_tags),
requirements=raw_labels.get("requirements", None),
tag=tag,
)
repository.versions[tag] = new_version
except KeyError as error:
raise Exception(f"unable to parse repository {repository}: {error}") from error
+ # sort the versions, with the highest version first
+ repository.versions = dict(
+ sorted(repository.versions.items(), key=lambda i: self.valid_semver(i[0]), reverse=True) # type: ignore
+ )
self.consolidated_data.append(repository)
with open("manifest.json", "w", encoding="utf-8") as manifest_file:
diff --git a/website/src/components/Counter.vue b/website/src/components/Counter.vue
index b2448b8..b90da8b 100644
--- a/website/src/components/Counter.vue
+++ b/website/src/components/Counter.vue
@@ -1,33 +1,48 @@
-
-
-
-
-
-
-
-
-
-
- {{ extension.name }}
-
-
- {{ extension.identifier }}
-
-
-
-
- {{ extension.description }}
-
-
+
+
+ {{ section.replace("-"," ").replace(/\b\w/g, s => s.toUpperCase()) + "s" }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ extension.name }}
+
+
+ {{ extension.identifier }}
+
+
+
+
+ {{ tag }}
+
+
+
+
+ {{ extension.description }}
+
+
+
+
diff --git a/website/src/types/manifest.ts b/website/src/types/manifest.ts
index 9b6c607..20e132f 100644
--- a/website/src/types/manifest.ts
+++ b/website/src/types/manifest.ts
@@ -9,6 +9,13 @@ export interface Company {
email?: string;
}
+export enum ExtensionType {
+ DEVICE_INTEGRATION = "device-integration",
+ THEME = "theme",
+ OTHER = "other",
+ EXAMPLE = "example"
+}
+
export interface Version {
permissions?: {[key: string]: any};
requirements?: string;
@@ -19,6 +26,9 @@ export interface Version {
readme?: string;
company?: Company;
support?: string;
+ type: ExtensionType;
+ filter_tags: string[];
+ extra_links: {[key: string]: string};
}
export interface RepositoryEntry {