Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2b4fce6
Add GAIA Agents playbook: Build Your First Agent with GAIA (#83)
kovtcharov-amd Mar 17, 2026
b38a37a
Set gaia as developed (#150)
danielholanda Mar 18, 2026
6a0260e
Comfyui Image Gen tests (Windows / Linux) (#153)
sreeram-11 Mar 18, 2026
e140d7f
Added automated tests for the lmstudio-rocm-llms playbook (#92)
sreeram-11 Mar 18, 2026
ec78e7e
Fix Pytorch display bug (#157)
danielholanda Mar 18, 2026
92fb703
Adding hidden=True for hiding tests (#156)
sreeram-11 Mar 18, 2026
4d6b276
Ensure a default device is always selected (#158)
danielholanda Mar 18, 2026
9ff8002
Adam/pytorch rocm changes (#136)
adamlam2-amd Mar 18, 2026
4bc1017
Fix pytorch formatting (#159)
danielholanda Mar 18, 2026
a230d4f
Update runner label for testing from 04 to 05 for the new machine (#160)
vgodsoe Mar 19, 2026
42c399e
Add AMD copyright to files (#149)
vgodsoe Mar 19, 2026
98392a2
Fix nesting issues (#162)
danielholanda Mar 19, 2026
bf1b705
Only show up preinstalled when needed (#163)
danielholanda Mar 19, 2026
35dba6f
Always show device selector (#164)
danielholanda Mar 19, 2026
dddd998
Dedup tests (#165)
danielholanda Mar 19, 2026
8f3b803
Add playbook-specific device support (#166)
danielholanda Mar 19, 2026
1957538
comfyui dependency fix (#154)
adamlam2-amd Mar 19, 2026
ba022b4
Refactor device selection (#168)
danielholanda Mar 20, 2026
e4d47a5
Add a playbook of llama factory fine-tuning (#70)
zhangnju Mar 20, 2026
7a2f55a
Max auio size updated (#169)
danielholanda Mar 20, 2026
a846ff3
Set as enabled (#171)
danielholanda Mar 20, 2026
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
4 changes: 4 additions & 0 deletions .github/scripts/fetch_github_issues.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
#!/usr/bin/env python3
# Copyright Advanced Micro Devices, Inc.
#
# SPDX-License-Identifier: MIT

import os
import sys
import json
Expand Down
157 changes: 92 additions & 65 deletions .github/scripts/run_playbook_tests.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
#!/usr/bin/env python3
# Copyright Advanced Micro Devices, Inc.
#
# SPDX-License-Identifier: MIT

"""
Playbook Test Runner
====================
Expand Down Expand Up @@ -207,7 +211,9 @@ def extract_setup_definitions(content: str) -> dict[str, dict[str, str]]:
"""Extract reusable @setup definitions from README content.

Supports @setup definitions wrapped in @os: blocks, where the platform is
inferred from the surrounding tag:
inferred from the surrounding tag. Handles arbitrarily nested ``@os:``
blocks (e.g. when ``@require`` injects dependency content that itself
contains ``@os:`` sections) by using a stack-based parser.

<!-- @os:windows -->
<!-- @setup:id=activate-venv command="llm-env\\Scripts\\activate.bat" -->
Expand All @@ -223,79 +229,50 @@ def extract_setup_definitions(content: str) -> dict[str, dict[str, str]]:
{"activate-venv": {"linux": "source llm-env/bin/activate", "windows": "llm-env\\Scripts\\activate.bat"}}
"""
setup_defs: dict[str, dict[str, str]] = {}

# First, extract @setup definitions inside @os: blocks
os_block_pattern = r"<!-- @os:(windows|linux) -->([\s\S]*?)<!-- @os:end -->"
setup_pattern = r"<!-- @setup:([^>]+) -->"

# Track which character ranges are inside @os: blocks
os_ranges: list[tuple[int, int]] = []

for os_match in re.finditer(os_block_pattern, content):
platform = os_match.group(1)
block_content = os_match.group(2)
block_start = os_match.start()
os_ranges.append((os_match.start(), os_match.end()))

for setup_match in re.finditer(setup_pattern, block_content):
attr_string = setup_match.group(1)
attrs = parse_test_attributes(attr_string)

setup_id = attrs.get("id")
if not setup_id:
abs_pos = block_start + setup_match.start()
line_number = content[:abs_pos].count("\n") + 1
print(
f"Warning: @setup definition at line {line_number} missing 'id', skipping"
)
continue

command = attrs.get("command")
if not command:
abs_pos = block_start + setup_match.start()
line_number = content[:abs_pos].count("\n") + 1
print(
f"Warning: @setup '{setup_id}' at line {line_number} has no command"
)
continue

if setup_id not in setup_defs:
setup_defs[setup_id] = {}
setup_defs[setup_id][platform] = command
os_blocks = _find_nested_blocks(
content,
r"<!-- @os:(windows|linux) -->",
"<!-- @os:end -->",
)

# Then, find @setup definitions outside any @os: block (applies to all platforms)
for setup_match in re.finditer(setup_pattern, content):
# Skip if this match falls within an @os: block
match_pos = setup_match.start()
inside_os_block = any(
start <= match_pos < end for start, end in os_ranges
)
if inside_os_block:
continue

attr_string = setup_match.group(1)
attrs = parse_test_attributes(attr_string)

setup_id = attrs.get("id")
if not setup_id:
line_number = content[: match_pos].count("\n") + 1
line_number = content[:match_pos].count("\n") + 1
print(
f"Warning: @setup definition at line {line_number} missing 'id', skipping"
)
continue

command = attrs.get("command")
if not command:
line_number = content[: match_pos].count("\n") + 1
line_number = content[:match_pos].count("\n") + 1
print(
f"Warning: @setup '{setup_id}' at line {line_number} has no command"
)
continue

# Determine platform from the innermost enclosing @os: block
platform = None
for value, start, end in os_blocks:
if start <= match_pos < end:
platform = value
break # blocks are sorted innermost-first

if setup_id not in setup_defs:
setup_defs[setup_id] = {}
setup_defs[setup_id]["linux"] = command
setup_defs[setup_id]["windows"] = command

if platform:
setup_defs[setup_id][platform] = command
else:
setup_defs[setup_id]["linux"] = command
setup_defs[setup_id]["windows"] = command

return setup_defs

Expand Down Expand Up @@ -369,31 +346,72 @@ def _replace_require(match: re.Match) -> str:
return re.sub(require_pattern, _replace_require, content)


def _find_nested_blocks(
content: str,
open_pattern: str,
close_literal: str,
) -> list[tuple[str, int, int]]:
"""Find properly nested tag blocks using a stack-based parser.

Returns a list of ``(value, block_start, block_end)`` tuples sorted by
span size ascending (innermost first). ``value`` is the capture group
from the opening tag (e.g. ``"linux"`` for ``<!-- @os:linux -->``).
"""
open_re = re.compile(open_pattern)
close_re = re.compile(re.escape(close_literal))

events: list[tuple[int, str, str]] = [] # (pos, 'open'|'close', value)
for m in open_re.finditer(content):
events.append((m.start(), "open", m.group(1)))
for m in close_re.finditer(content):
events.append((m.start(), "close", ""))
events.sort(key=lambda e: e[0])

stack: list[tuple[str, int]] = [] # (value, start_pos)
blocks: list[tuple[str, int, int]] = []
for pos, kind, value in events:
if kind == "open":
stack.append((value, pos))
elif kind == "close" and stack:
open_value, open_pos = stack.pop()
close_end = pos + len(close_literal)
blocks.append((open_value, open_pos, close_end))

blocks.sort(key=lambda b: b[2] - b[1])
return blocks


def _infer_platform(content: str, position: int) -> str:
"""Infer the platform for a test based on surrounding @os: tags.

If the test is inside an ``<!-- @os:windows -->`` block, returns "windows".
If inside an ``<!-- @os:linux -->`` block, returns "linux".
Otherwise returns "all".
Handles arbitrarily nested ``@os:`` blocks by finding the innermost
enclosing block that contains *position*.
"""
os_block_pattern = r"<!-- @os:(windows|linux) -->([\s\S]*?)<!-- @os:end -->"
for os_match in re.finditer(os_block_pattern, content):
if os_match.start() <= position < os_match.end():
return os_match.group(1)
blocks = _find_nested_blocks(
content,
r"<!-- @os:(windows|linux) -->",
"<!-- @os:end -->",
)
for value, start, end in blocks:
if start <= position < end:
return value
return "all"


def _infer_device(content: str, position: int) -> str:
"""Infer the target device(s) for a test based on surrounding @device: tags.

If the test is inside a ``<!-- @device:halo,stx -->`` block, returns "halo,stx".
If inside ``<!-- @device:halo -->`` returns "halo".
Otherwise returns "all".
Handles arbitrarily nested ``@device:`` blocks by finding the innermost
enclosing block that contains *position*.
"""
device_block_pattern = r"<!-- @device:([\w,]+) -->([\s\S]*?)<!-- @device:end -->"
for m in re.finditer(device_block_pattern, content):
if m.start() <= position < m.end():
return m.group(1)
blocks = _find_nested_blocks(
content,
r"<!-- @device:([\w,]+) -->",
"<!-- @device:end -->",
)
for value, start, end in blocks:
if start <= position < end:
return value
return "all"


Expand Down Expand Up @@ -473,7 +491,16 @@ def extract_tests(readme_path: Path, target_platform: str, target_device: Option

tests.append(test)

return tests
# Deduplicate tests by ID, keeping the first occurrence (README order).
# Duplicates arise when @require tags inline the same dependency content
# into multiple @os: blocks, or when device variants share an ID.
seen_ids: set[str] = set()
unique_tests: list[TestBlock] = []
for t in tests:
if t.id not in seen_ids:
seen_ids.add(t.id)
unique_tests.append(t)
return unique_tests


def run_test(
Expand Down
56 changes: 41 additions & 15 deletions .github/scripts/validate_playbooks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
#!/usr/bin/env python3
# Copyright Advanced Micro Devices, Inc.
#
# SPDX-License-Identifier: MIT

"""
Playbook Validation Script

Expand All @@ -7,7 +11,7 @@
2. Follow the contract defined in website/src/types/playbook.ts
3. Have valid JSON in playbook.json
4. Have consistent IDs (folder name should match id in playbook.json)
5. Have asset files within size limits (max 500 KB per file)
5. Have asset files within size limits (max 500 KB per file, 5 MB for audio)
"""

import json
Expand All @@ -34,7 +38,15 @@
ALLOWED_ITEMS = {"README.md", "playbook.json", "platform.md", "assets"}

# Required fields in playbook.json (based on PlaybookMeta interface)
REQUIRED_FIELDS = ["id", "title", "description", "time", "platforms", "developed", "published"]
REQUIRED_FIELDS = [
"id",
"title",
"description",
"time",
"supported_platforms",
"developed",
"published",
]

# Valid values for enum fields
VALID_PLATFORMS = ["windows", "linux"]
Expand All @@ -43,6 +55,9 @@
# Asset constraints
MAX_ASSET_SIZE_KB = 500
MAX_ASSET_SIZE_BYTES = MAX_ASSET_SIZE_KB * 1024
AUDIO_EXTENSIONS = {".mp3", ".wav"}
MAX_AUDIO_SIZE_KB = 5 * 1024
MAX_AUDIO_SIZE_BYTES = MAX_AUDIO_SIZE_KB * 1024

# ============================================================================
# Styling
Expand Down Expand Up @@ -170,15 +185,19 @@ def validate_asset_sizes(
if not asset.is_file():
continue

is_audio = asset.suffix.lower() in AUDIO_EXTENSIONS
max_bytes = MAX_AUDIO_SIZE_BYTES if is_audio else MAX_ASSET_SIZE_BYTES
max_kb = MAX_AUDIO_SIZE_KB if is_audio else MAX_ASSET_SIZE_KB

file_size = asset.stat().st_size
if file_size > MAX_ASSET_SIZE_BYTES:
if file_size > max_bytes:
size_kb = file_size / 1024
result.add_error(
playbook_name,
f'Asset file too large: "{asset.name}"\n'
f" Size: {size_kb:.1f} KB\n"
f" Maximum allowed: {MAX_ASSET_SIZE_KB} KB\n"
" Please compress or resize the image.",
f" Maximum allowed: {max_kb} KB\n"
" Please compress or resize the file.",
)


Expand Down Expand Up @@ -258,26 +277,33 @@ def validate_playbook_json(
f'Field "published" must be a boolean, got: {type(meta["published"]).__name__}',
)

# Validate platforms array
if "platforms" in meta:
if not isinstance(meta["platforms"], list):
# Validate supported_platforms (device -> os[] map)
if "supported_platforms" in meta:
if not isinstance(meta["supported_platforms"], dict):
result.add_error(
playbook_name,
f'Field "platforms" must be an array, got: {type(meta["platforms"]).__name__}',
f'Field "supported_platforms" must be an object (device → OS[]), got: {type(meta["supported_platforms"]).__name__}',
)
else:
if len(meta["platforms"]) == 0:
if len(meta["supported_platforms"]) == 0:
result.add_error(
playbook_name,
'Field "platforms" cannot be empty - at least one platform is required',
'Field "supported_platforms" cannot be empty - at least one device/platform combo is required',
)
for platform in meta["platforms"]:
if platform not in VALID_PLATFORMS:
for device, os_list in meta["supported_platforms"].items():
if not isinstance(os_list, list):
result.add_error(
playbook_name,
f'Invalid platform: "{platform}"\n'
f" Valid platforms: {', '.join(VALID_PLATFORMS)}",
f'supported_platforms["{device}"] must be an array, got: {type(os_list).__name__}',
)
else:
for platform in os_list:
if platform not in VALID_PLATFORMS:
result.add_error(
playbook_name,
f'Invalid platform in supported_platforms["{device}"]: "{platform}"\n'
f" Valid platforms: {', '.join(VALID_PLATFORMS)}",
)

# Validate difficulty
if "difficulty" in meta and meta["difficulty"] not in VALID_DIFFICULTIES:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/check-website.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Copyright Advanced Micro Devices, Inc.
#
# SPDX-License-Identifier: MIT

name: Check Website

on:
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/fetch-github-issues.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Copyright Advanced Micro Devices, Inc.
#
# SPDX-License-Identifier: MIT

name: Fetch GitHub Issues

on:
Expand Down Expand Up @@ -28,7 +32,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '3.12'

- name: Fetch GitHub issues
env:
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/monitor-runners.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Copyright Advanced Micro Devices, Inc.
#
# SPDX-License-Identifier: MIT

name: Monitor Self-Hosted Runners

on:
Expand All @@ -17,7 +21,7 @@ jobs:
id: runners
run: |
# Use the same runner list as the heartbeat workflow
runners="xsj-aimlab-halo-0,xsj-aimlab-halo-1,xsj-aimlab-halo-02,xsj-aimlab-halo-03,xsj-aimlab-stxp-01,xsj-aimlab-stxp-02,xsj-aimlab-stxp-03,xsj-aimlab-stxp-04,xsj-aimlab-krk-01,xsj-aimlab-krk-02,xsj-aimlab-krk-03,xsj-aimlab-krk-04,APEXX-T4P-03,tp401-linux-r9700-vm,tp401-linux-w7900-vm,xsj-aimlab-radeon-7900-vm01"
runners="xsj-aimlab-halo-0,xsj-aimlab-halo-1,xsj-aimlab-halo-02,xsj-aimlab-halo-03,xsj-aimlab-stxp-01,xsj-aimlab-stxp-02,xsj-aimlab-stxp-03,xsj-aimlab-stxp-05,xsj-aimlab-krk-01,xsj-aimlab-krk-02,xsj-aimlab-krk-03,xsj-aimlab-krk-04,APEXX-T4P-03,tp401-linux-r9700-vm,tp401-linux-w7900-vm,xsj-aimlab-radeon-7900-vm01"

echo "names=$runners" >> $GITHUB_OUTPUT
echo "Using static runner list: $runners"
Expand Down
Loading
Loading