Skip to content

Commit 6abe032

Browse files
authored
Merge pull request #4 from rhdh-parasol/catalog/software-catalog-info
Add software-catalog catalog-info.yaml for overlay workspaces
2 parents 6d28288 + 7b713a9 commit 6abe032

69 files changed

Lines changed: 2589 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/validate-catalog-scripts.yaml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ on:
1818
- 'scripts/tests/**'
1919
- 'scripts/requirements-dev.txt'
2020
- '.github/workflows/validate-catalog-scripts.yaml'
21+
- 'catalog-info.yaml'
22+
- 'workspaces/*/catalog-info.yaml'
2123
workflow_dispatch:
2224

2325
concurrency:
@@ -46,3 +48,6 @@ jobs:
4648
- name: Run tests
4749
working-directory: scripts
4850
run: python -m pytest tests/ -v
51+
52+
- name: Check generated software-catalog catalog-info.yaml
53+
run: python scripts/generateCatalogInfo.py --check

‎catalog-info.yaml‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by scripts/generateCatalogInfo.py. Do not edit by hand.
2+
# Re-run: python3 scripts/generateCatalogInfo.py
3+
apiVersion: backstage.io/v1alpha1
4+
kind: Component
5+
metadata:
6+
name: rhdh-plugin-export-overlays
7+
title: RHDH Plugin Export Overlays
8+
description: Packaging and OCI publish layer for RHDH dynamic plugins. Contains 65 overlay workspaces.
9+
annotations:
10+
github.com/project-slug: rhdh-parasol/rhdh-plugin-export-overlays
11+
tags:
12+
- overlay
13+
- dynamic-plugin
14+
- repository
15+
links:
16+
- url: https://github.com/rhdh-parasol/rhdh-plugin-export-overlays
17+
title: GitHub repository
18+
icon: github
19+
type: source
20+
- url: https://github.com/rhdh-parasol/rhdh-plugin-export-overlays/actions/workflows/publish-workspace-plugins.yaml
21+
title: Publish workflow
22+
type: ci
23+
spec:
24+
type: repository
25+
lifecycle: experimental
26+
owner: rhdh-team
27+
system: rhdh

‎scripts/generateCatalogInfo.py‎

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) Red Hat, Inc.
4+
# This program and the accompanying materials are made
5+
# available under the terms of the Eclipse Public License 2.0
6+
# which is available at https://www.eclipse.org/legal/epl-2.0/
7+
#
8+
# SPDX-License-Identifier: EPL-2.0
9+
#
10+
# Generate software-catalog catalog-info.yaml files for this overlay
11+
# repository. These are Backstage kind: Component descriptors consumed by
12+
# GithubEntityProvider (path /workspaces/*/catalog-info.yaml), not the
13+
# Extensions Catalog Plugin/Package entities under metadata/ and
14+
# catalog-entities/extensions/.
15+
#
16+
# Usage:
17+
# python3 scripts/generateCatalogInfo.py
18+
# python3 scripts/generateCatalogInfo.py --check
19+
20+
from __future__ import annotations
21+
22+
import argparse
23+
import json
24+
import sys
25+
from pathlib import Path
26+
27+
import yaml
28+
29+
from plugin_utils import (
30+
log_error,
31+
log_info,
32+
log_warn,
33+
read_plugins_list,
34+
set_debug,
35+
)
36+
37+
GENERATED_HEADER = """\
38+
# Generated by scripts/generateCatalogInfo.py. Do not edit by hand.
39+
# Re-run: python3 scripts/generateCatalogInfo.py
40+
"""
41+
42+
DEFAULT_OVERLAY_SLUG = "rhdh-parasol/rhdh-plugin-export-overlays"
43+
ROOT_COMPONENT_NAME = "rhdh-plugin-export-overlays"
44+
45+
# Software-catalog Component metadata.name in rhdh-plugins workspaces/*/catalog-info.yaml.
46+
# Only set spec.dependsOn when the source plugin already has a Component in the
47+
# Parasol software catalog. Overlay workspaces sourced from community-plugins
48+
# (or other repos) are omitted on purpose.
49+
RHDH_PLUGINS_COMPONENT_BY_WORKSPACE: dict[str, str] = {
50+
"adoption-insights": "adoption-insights",
51+
"ai-integrations": "ai-integrations",
52+
"app-defaults": "app-defaults",
53+
"boost": "boost",
54+
"bulk-import": "red-hat-developer-hub-bulk-import",
55+
"cost-management": "cost-management",
56+
"extensions": "red-hat-developer-hub-extensions",
57+
"global-header": "red-hat-developer-hub-global-header",
58+
"homepage": "red-hat-developer-hub-homepage",
59+
"intelligent-assistant": "developer-intelligent-assistant",
60+
"mcp-integrations": "mcp-integrations",
61+
"orchestrator": "red-hat-developer-hub-orchestrator",
62+
"quickstart": "quickstart",
63+
"scorecard": "scorecard",
64+
"theme": "red-hat-developer-hub-theme",
65+
"translations": "translations",
66+
"x2a": "red-hat-developer-hub-x2a-frontend",
67+
}
68+
69+
70+
def overlay_entity_name(workspace: str) -> str:
71+
return f"overlay-{workspace}"
72+
73+
74+
def is_rhdh_plugins_source(repo: str) -> bool:
75+
return repo.rstrip("/").endswith("/rhdh-plugins")
76+
77+
78+
def source_tree_url(source: dict, workspace: str) -> str:
79+
repo = source["repo"].rstrip("/")
80+
ref = source.get("repo-ref") or "main"
81+
if source.get("repo-flat"):
82+
return f"{repo}/tree/{ref}"
83+
return f"{repo}/tree/{ref}/workspaces/{workspace}"
84+
85+
86+
def overlay_tree_url(overlay_slug: str, workspace: str) -> str:
87+
return f"https://github.com/{overlay_slug}/tree/main/workspaces/{workspace}"
88+
89+
90+
def export_workflow_url(overlay_slug: str) -> str:
91+
return (
92+
f"https://github.com/{overlay_slug}/actions/workflows/"
93+
"publish-workspace-plugins.yaml"
94+
)
95+
96+
97+
def rhdh_plugins_depends_on(workspace: str) -> str | None:
98+
return RHDH_PLUGINS_COMPONENT_BY_WORKSPACE.get(workspace)
99+
100+
101+
def load_source_json(workspace_dir: Path) -> dict:
102+
source_file = workspace_dir / "source.json"
103+
with source_file.open(encoding="utf-8") as handle:
104+
return json.load(handle)
105+
106+
107+
class _IndentDumper(yaml.SafeDumper):
108+
"""Keep list items indented under their parent key."""
109+
110+
def increase_indent(self, flow=False, indentless=False):
111+
return super().increase_indent(flow, False)
112+
113+
114+
def dump_entity(entity: dict) -> str:
115+
body = yaml.dump(
116+
entity,
117+
Dumper=_IndentDumper,
118+
sort_keys=False,
119+
default_flow_style=False,
120+
allow_unicode=True,
121+
width=120,
122+
)
123+
return GENERATED_HEADER + body
124+
125+
126+
def build_root_entity(overlay_slug: str, workspace_count: int) -> dict:
127+
return {
128+
"apiVersion": "backstage.io/v1alpha1",
129+
"kind": "Component",
130+
"metadata": {
131+
"name": ROOT_COMPONENT_NAME,
132+
"title": "RHDH Plugin Export Overlays",
133+
"description": (
134+
"Packaging and OCI publish layer for RHDH dynamic plugins. "
135+
f"Contains {workspace_count} overlay workspaces."
136+
),
137+
"annotations": {
138+
"github.com/project-slug": overlay_slug,
139+
},
140+
"tags": ["overlay", "dynamic-plugin", "repository"],
141+
"links": [
142+
{
143+
"url": f"https://github.com/{overlay_slug}",
144+
"title": "GitHub repository",
145+
"icon": "github",
146+
"type": "source",
147+
},
148+
{
149+
"url": export_workflow_url(overlay_slug),
150+
"title": "Publish workflow",
151+
"type": "ci",
152+
},
153+
],
154+
},
155+
"spec": {
156+
"type": "repository",
157+
"lifecycle": "experimental",
158+
"owner": "rhdh-team",
159+
"system": "rhdh",
160+
},
161+
}
162+
163+
164+
def build_workspace_entity(
165+
workspace: str,
166+
source: dict,
167+
plugin_paths: list[str],
168+
overlay_slug: str,
169+
) -> dict:
170+
plugin_count = len(plugin_paths)
171+
package_word = "package" if plugin_count == 1 else "packages"
172+
ref = source.get("repo-ref") or "main"
173+
short_ref = ref[:7] if len(ref) >= 7 else ref
174+
entity: dict = {
175+
"apiVersion": "backstage.io/v1alpha1",
176+
"kind": "Component",
177+
"metadata": {
178+
"name": overlay_entity_name(workspace),
179+
"title": f"Overlay workspace {workspace}",
180+
"description": (
181+
f"Dynamic-plugin export overlay for workspace '{workspace}' "
182+
f"({plugin_count} {package_word}). "
183+
f"Upstream source pinned at {short_ref}."
184+
),
185+
"annotations": {
186+
"github.com/project-slug": overlay_slug,
187+
},
188+
"tags": ["overlay", "dynamic-plugin"],
189+
"links": [
190+
{
191+
"url": overlay_tree_url(overlay_slug, workspace),
192+
"title": "Overlay workspace",
193+
"icon": "github",
194+
"type": "source",
195+
},
196+
{
197+
"url": source_tree_url(source, workspace),
198+
"title": "Upstream source",
199+
"icon": "github",
200+
"type": "upstream",
201+
},
202+
{
203+
"url": export_workflow_url(overlay_slug),
204+
"title": "Latest export workflow",
205+
"type": "ci",
206+
},
207+
],
208+
},
209+
"spec": {
210+
"type": "rhdh-overlay-workspace",
211+
"lifecycle": "experimental",
212+
"owner": "rhdh-team",
213+
"system": "rhdh",
214+
"subcomponentOf": ROOT_COMPONENT_NAME,
215+
},
216+
}
217+
depends_on = rhdh_plugins_depends_on(workspace)
218+
if depends_on and is_rhdh_plugins_source(source.get("repo", "")):
219+
entity["spec"]["dependsOn"] = [f"component:default/{depends_on}"]
220+
return entity
221+
222+
223+
def discover_workspaces(repo_root: Path) -> list[Path]:
224+
workspaces_dir = repo_root / "workspaces"
225+
if not workspaces_dir.is_dir():
226+
return []
227+
found = []
228+
for path in sorted(workspaces_dir.iterdir()):
229+
if path.is_dir() and (path / "source.json").is_file():
230+
found.append(path)
231+
return found
232+
233+
234+
def planned_files(
235+
repo_root: Path, overlay_slug: str
236+
) -> list[tuple[Path, str]]:
237+
workspaces = discover_workspaces(repo_root)
238+
files: list[tuple[Path, str]] = [
239+
(
240+
repo_root / "catalog-info.yaml",
241+
dump_entity(build_root_entity(overlay_slug, len(workspaces))),
242+
)
243+
]
244+
for workspace_dir in workspaces:
245+
source = load_source_json(workspace_dir)
246+
plugin_paths = read_plugins_list(workspace_dir)
247+
entity = build_workspace_entity(
248+
workspace_dir.name, source, plugin_paths, overlay_slug
249+
)
250+
files.append((workspace_dir / "catalog-info.yaml", dump_entity(entity)))
251+
return files
252+
253+
254+
def write_files(files: list[tuple[Path, str]], dry_run: bool) -> None:
255+
for path, content in files:
256+
if dry_run:
257+
log_info(f"would write {path}")
258+
continue
259+
path.write_text(content, encoding="utf-8")
260+
log_info(f"wrote {path}")
261+
262+
263+
def check_files(files: list[tuple[Path, str]]) -> int:
264+
stale = []
265+
for path, content in files:
266+
if not path.is_file():
267+
stale.append(f"missing {path}")
268+
continue
269+
if path.read_text(encoding="utf-8") != content:
270+
stale.append(f"stale {path}")
271+
if stale:
272+
for item in stale:
273+
log_error(item)
274+
log_error(
275+
f"{len(stale)} catalog-info.yaml file(s) out of date. "
276+
"Re-run: python3 scripts/generateCatalogInfo.py"
277+
)
278+
return 1
279+
log_info(f"{len(files)} catalog-info.yaml file(s) up to date")
280+
return 0
281+
282+
283+
def repo_root_from_script() -> Path:
284+
return Path(__file__).resolve().parent.parent
285+
286+
287+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
288+
parser = argparse.ArgumentParser(
289+
description=(
290+
"Generate software-catalog catalog-info.yaml files for overlay "
291+
"workspaces."
292+
)
293+
)
294+
parser.add_argument(
295+
"--repo-root",
296+
type=Path,
297+
default=None,
298+
help="Overlay repository root (default: parent of scripts/)",
299+
)
300+
parser.add_argument(
301+
"--overlay-slug",
302+
default=DEFAULT_OVERLAY_SLUG,
303+
help=f"GitHub org/repo for this overlay fork (default: {DEFAULT_OVERLAY_SLUG})",
304+
)
305+
parser.add_argument(
306+
"--check",
307+
action="store_true",
308+
help="Exit 1 if generated files would change (do not write)",
309+
)
310+
parser.add_argument(
311+
"--dry-run",
312+
action="store_true",
313+
help="Print paths that would be written, do not write",
314+
)
315+
parser.add_argument("--debug", action="store_true")
316+
return parser.parse_args(argv)
317+
318+
319+
def main(argv: list[str] | None = None) -> int:
320+
args = parse_args(argv)
321+
set_debug(args.debug)
322+
repo_root = (args.repo_root or repo_root_from_script()).resolve()
323+
if not (repo_root / "workspaces").is_dir():
324+
log_error(f"no workspaces/ directory under {repo_root}")
325+
return 1
326+
327+
files = planned_files(repo_root, args.overlay_slug)
328+
if not files:
329+
log_warn("no overlay workspaces with source.json found")
330+
return 1
331+
332+
if args.check:
333+
return check_files(files)
334+
335+
write_files(files, dry_run=args.dry_run)
336+
log_info(f"{len(files)} catalog-info.yaml file(s)")
337+
return 0
338+
339+
340+
if __name__ == "__main__":
341+
sys.exit(main())

0 commit comments

Comments
 (0)