Skip to content
Closed
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
85 changes: 85 additions & 0 deletions .github/ISSUE_TEMPLATE/product-repo-approval.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Product repo approval
description: Get engineering and product sign-off on a product repo before its skills enter the catalog.
title: "[Product repo approval] <owner/repo>"
labels:
- product-repo-approval
body:
- type: markdown
attributes:
value: |
File this **before** opening a catalog pull request. Two people have to sign off here:

- the **engineering owner**, confirming they own the underlying product, and
- the **product manager** for that product, confirming they are comfortable with
the product repo backing these skills and that the product is in Tech Preview or GA.

Each of them signs off by leaving a comment containing `/approve` — say as much or as
little alongside it as you like. Approvals are recorded per person: nobody can approve
on someone else's behalf, and filing this issue is not an approval.

Once both have approved, a pull request is opened automatically adding this repo to
`.github/skill_owners.json`. Merging that pull request closes this issue.

- type: input
id: product
attributes:
label: Product name
description: The product as it is known to customers, not the repo name.
placeholder: e.g. TraceLens
validations:
required: true

- type: input
id: repo
attributes:
label: Product repo
description: The AMD-owned repo that holds the skills, as `owner/repo`. This is the repo being approved.
placeholder: e.g. AMD-AGI/TraceLens
validations:
required: true

- type: textarea
id: skills
attributes:
label: Skills to add
description: One per line, as the catalog will name them. Rough names are fine if they are not settled yet.
placeholder: |
tracelens-analysis-orchestrator
validations:
required: true

- type: input
id: engineering_owner
attributes:
label: Engineering owner
description: GitHub handle of the person who owns the underlying product. They must approve from this account, so get the handle exactly right — edit the issue if it is wrong.
placeholder: e.g. @octocat
validations:
required: true

- type: input
id: product_manager
attributes:
label: Product manager
description: GitHub handle of the product manager who manages this product. They must approve from this account.
placeholder: e.g. @octocat
validations:
required: true

- type: dropdown
id: status
attributes:
label: Product status
description: Approval requires Tech Preview or GA. If the product is not there yet, file this once it is.
options:
- "Tech Preview"
- "GA"
- "Neither yet — pre-release or internal only"
validations:
required: true

- type: textarea
id: notes
attributes:
label: Notes
description: Anything the approvers should know, or context on the product's status.
129 changes: 129 additions & 0 deletions .github/scripts/record_skill_owner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Record an approved product repo in `.github/skill_owners.json`.

Called by the `product-repo-approval` workflow once both the engineering owner
and the product manager named on an approval issue have approved. The workflow
puts the result up for review as a pull request, so this script only edits the
registry; it never decides whether an approval is valid.

Registry schema:

{
"repos": [
{
"repo": "AMD-AGI/TraceLens", # owner/repo, unique per entry
"product": "TraceLens", # customer-facing product name
"status": "tech-preview" | "ga", # product status at approval time
"engineering_owner": "octocat", # GitHub handle, no leading @
"product_manager": "octocat", # GitHub handle, no leading @
"approved_in": "<approval issue URL>",
"approved_at": "2026-09-01T17:04:22Z"
}
]
}

Entries are keyed on `repo` and kept sorted by it. Approving a repo that is
already listed updates the existing entry in place rather than adding a second
one, so a re-approval after an ownership or status change reads as the current
truth and `approved_at` moves to the later approval.
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path

REPO_PATTERN = re.compile(r"^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$")
STATUSES = ("tech-preview", "ga")
DEFAULT_REGISTRY = Path(__file__).resolve().parents[1] / "skill_owners.json"


def handle(value: str) -> str:
"""Normalize a GitHub handle for storage: no leading @, no stray spaces."""
return value.strip().lstrip("@").strip()


def load_registry(path: Path) -> dict:
if not path.exists():
return {"repos": []}
try:
data = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as err:
raise SystemExit(f"{path} is not valid JSON: {err}") from err
if not isinstance(data, dict) or not isinstance(data.get("repos", []), list):
raise SystemExit(f"{path} must be an object with a 'repos' array.")
data.setdefault("repos", [])
return data


def write_registry(path: Path, data: dict) -> None:
data["repos"].sort(key=lambda entry: entry.get("repo", "").lower())
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--repo", required=True, help="Product repo as owner/repo.")
parser.add_argument("--product", required=True, help="Customer-facing product name.")
parser.add_argument(
"--status", required=True, choices=STATUSES, help="Product status at approval time."
)
parser.add_argument(
"--engineering-owner", required=True, help="GitHub handle of the engineering owner."
)
parser.add_argument(
"--product-manager", required=True, help="GitHub handle of the product manager."
)
parser.add_argument(
"--issue", required=True, help="URL of the approval issue the sign-off happened on."
)
parser.add_argument(
"--registry",
type=Path,
default=DEFAULT_REGISTRY,
help=f"Registry file to edit (default: {DEFAULT_REGISTRY}).",
)
return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)

repo = args.repo.strip()
if not REPO_PATTERN.match(repo):
raise SystemExit(f"'{repo}' is not an owner/repo slug.")

entry = {
"repo": repo,
"product": args.product.strip(),
"status": args.status,
"engineering_owner": handle(args.engineering_owner),
"product_manager": handle(args.product_manager),
"approved_in": args.issue.strip(),
"approved_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
}
for field in ("product", "engineering_owner", "product_manager", "approved_in"):
if not entry[field]:
raise SystemExit(f"--{field.replace('_', '-')} cannot be empty.")

registry = load_registry(args.registry)
existing = next(
(e for e in registry["repos"] if e.get("repo", "").lower() == repo.lower()), None
)
if existing is None:
registry["repos"].append(entry)
print(f"Added {repo} to {args.registry.name} ({entry['status']}).")
else:
existing.update(entry)
print(f"Updated the existing {repo} entry in {args.registry.name} ({entry['status']}).")

write_registry(args.registry, registry)
return 0


if __name__ == "__main__":
sys.exit(main())
3 changes: 3 additions & 0 deletions .github/skill_owners.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"repos": []
}
Loading
Loading