diff --git a/scripts/README_LABEL_MIGRATION.md b/scripts/README_LABEL_MIGRATION.md
new file mode 100644
index 00000000000..4eb953ffbc7
--- /dev/null
+++ b/scripts/README_LABEL_MIGRATION.md
@@ -0,0 +1,386 @@
+# Label Migration Scripts
+
+This directory contains scripts for migrating GitHub labels to align with Rust project conventions and the new package structure from [issue #2209](https://github.com/ethereum/execution-spec-tests/issues/2209).
+
+## šÆ Goals
+
+1. **Align with Rust conventions**: Follow [Rust's label triaging guidelines](https://forge.rust-lang.org/release/issue-triaging.html)
+2. **Consistent grouping**: All labels in a group have the same prefix and color
+3. **New package structure**: Map labels to the reorganized package layout
+4. **Namespace separation**: Use `A-test-*` prefix to distinguish test framework labels from future spec labels
+
+## š Label Groups
+
+Following Rust project conventions:
+
+| Prefix | Color | Purpose | Example |
+|--------|-------|---------|---------|
+| `A-*` |  `#FBCA04` Yellow | Area (packages, features) | `A-test-tools`, `A-test-cli-fill` |
+| `C-*` |  `#C5DEF5` Light Blue | Category (bug, enhancement, etc.) | `C-bug`, `C-enhance` |
+| `E-*` |  `#02E10C` Bright Green | Experience/Difficulty | `E-easy`, `E-medium`, `E-hard` |
+| `F-*` |  `#D4C5F9` Purple | Ethereum Forks | `F-prague`, `F-osaka` |
+| `P-*` |  `#E99695` Pink | Priority | `P-low`, `P-medium`, `P-high`, `P-urgent` |
+| `S-*` |  `#D4C5F9` Purple | Status | `S-needs-discussion`, `S-needs-attention` |
+
+### Key Principles
+
+- Each issue/PR should have **at most one label from each group** (flexible for multi-area issues)
+- All labels in a group share the **same color** for visual consistency
+- Labels follow a **common prefix** pattern for easy filtering
+
+## š§ Scripts
+
+### 1. `label_mapping.py` - Main Migration Script
+
+The core script that handles label creation and migration.
+
+#### Configuration
+
+All label mappings are defined in the `LABEL_MAPPING` dictionary:
+
+```python
+LABEL_MAPPING = {
+ "old_label_name": Label("new_name", color, "description"), # Map old ā new
+ "label_to_keep": "KEEP", # Keep unchanged
+ "label_to_delete": None, # Delete without replacement
+}
+```
+
+**Easy to customize**: Just edit the dictionary to change mappings!
+
+#### Usage
+
+**Mode 1: Create (Safe - Testing)**
+```bash
+# Create new labels in a test repository
+uv run python scripts/label_mapping.py create --repo danceratopz/execution-specs-labels
+
+# Dry run (show what would be created)
+uv run python scripts/label_mapping.py create --repo danceratopz/execution-specs-labels --dry-run
+```
+
+**Mode 2: Migrate (Destructive - Production)**
+```bash
+# Full migration: create labels, update issues/PRs, delete old labels
+# ā ļø THIS MODIFIES THE REPO - USE WITH CAUTION
+uv run python scripts/label_mapping.py migrate --repo ethereum/execution-spec-tests
+
+# Dry run first!
+uv run python scripts/label_mapping.py migrate --repo ethereum/execution-spec-tests --dry-run
+```
+
+#### Key Mappings
+
+**Package Structure (A-test-*)**
+```
+scope:tools ā A-test-tools
+scope:forks ā A-test-forks
+scope:pytest ā A-test-pytest
+scope:exceptions ā A-test-exceptions
+...
+```
+
+**CLI Commands (A-test-cli-*)**
+```
+scope:fill ā A-test-cli-fill
+scope:consume ā A-test-cli-consume
+scope:execute ā A-test-cli-execute
+scope:eest ā A-test-cli-eest
+scope:make ā A-test-cli-make
+scope:gentest ā A-test-cli-gentest
+```
+
+**Categories (C-*)**
+```
+type:bug ā C-bug
+type:feat ā C-enhance
+type:refactor ā C-refactor
+type:chore ā C-chore
+type:test ā C-test
+type:docs ā C-doc
+question ā C-question
+```
+
+**Forks (F-*)**
+```
+fork:prague ā F-prague
+fork:osaka ā F-osaka
+fork:amsterdam ā F-amsterdam
+```
+
+**Status (S-*)**
+```
+needs-discussion ā S-needs-discussion
+needs-attention ā S-needs-attention
+```
+
+**Feature Consolidation**
+```
+feature:benchmark ā A-test-benchmark
+feature:zkevm ā A-test-benchmark # Consolidated
+feature:stateless ā A-test-benchmark # Consolidated
+feature:eof ā A-test-eof
+```
+
+**Standard GitHub Labels Kept**
+- `good first issue` - Updated to use `#02E10C` (bright green, matches `E-easy`)
+- `help wanted` - Kept as-is
+- `tracker` - Kept as-is
+- `automated issue` - Kept as-is
+- `report` - Kept as-is
+- `port` - Kept as-is
+
+### 2. `label_usage.py` - Analyze Label Usage
+
+Check how many times each label is used across issues and PRs.
+
+```bash
+uv run python scripts/label_usage.py
+```
+
+**Output:**
+- Labels with < 5 uses (candidates for deletion/consolidation)
+- Full usage report sorted by count
+- Helps identify unused or rarely-used labels
+
+**Example output:**
+```
+Labels with < 5 uses:
+ 1 | feature:stateless
+ 1 | invalid
+ 2 | duplicate
+ 4 | t8ntools
+```
+
+### 3. `generate_mapping_html.py` - Visual HTML Report
+
+Generate a beautiful HTML visualization of the label migration.
+
+```bash
+uv run python scripts/generate_mapping_html.py
+```
+
+**Output:** `mapping.html` with:
+- Color-coded label groups
+- Old ā New label mappings
+- Usage statistics (issues/PRs per label)
+- List of new labels
+- Labels kept unchanged
+
+**Sharing the HTML:**
+```bash
+# Create a GitHub Gist
+gh gist create mapping.html --public --desc "Label Migration Mapping"
+
+# View rendered HTML (replace GIST_ID with your gist ID)
+# https://htmlpreview.github.io/?https://gist.githubusercontent.com/USER/GIST_ID/raw/mapping.html
+```
+
+### 4. `generate_mapping_md.py` - Markdown Report
+
+Generate a Markdown report suitable for GitHub issues and Discord.
+
+```bash
+uv run python scripts/generate_mapping_md.py
+```
+
+**Output:** `mapping.md` with:
+- Markdown tables showing mappings
+- Usage statistics
+- Works perfectly in GitHub issues/comments
+- Copy/paste friendly for Discord
+
+## š Recommended Workflow
+
+### Step 1: Analyze Current Labels
+```bash
+# Check label usage
+uv run python scripts/label_usage.py
+```
+
+### Step 2: Customize Mappings
+
+Edit `scripts/label_mapping.py`:
+- Modify `LABEL_MAPPING` dictionary to change old ā new mappings
+- Update `NEW_LABELS` list to add labels without old mappings
+- Adjust `COLORS` dictionary if needed
+
+### Step 3: Generate Documentation
+
+```bash
+# Generate visual reports
+uv run python scripts/generate_mapping_html.py
+uv run python scripts/generate_mapping_md.py
+
+# Share for review
+gh gist create mapping.html --public
+```
+
+### Step 4: Test in Safe Repo
+
+```bash
+# Create labels in test repo
+uv run python scripts/label_mapping.py create --repo YOUR_USERNAME/test-repo
+
+# Review at: https://github.com/YOUR_USERNAME/test-repo/labels
+```
+
+### Step 5: Dry Run on Production
+
+```bash
+# See what would happen WITHOUT making changes
+uv run python scripts/label_mapping.py migrate --repo ethereum/execution-spec-tests --dry-run
+```
+
+### Step 6: Full Migration
+
+```bash
+# ā ļø DESTRUCTIVE - Make sure you're ready!
+uv run python scripts/label_mapping.py migrate --repo ethereum/execution-spec-tests
+```
+
+## š¦ Package Structure Mapping
+
+Based on [issue #2209](https://github.com/ethereum/execution-spec-tests/issues/2209):
+
+```
+src/ethereum_spec_tests/
+āāā base_types/ ā A-test-base_types
+āāā client_clis/ ā A-test-client-clis
+āāā config/ ā A-test-config
+āāā ethereum_test_cli/ ā A-test-ethereum_test_cli
+ā āāā eest ā A-test-cli-eest
+ā āāā fill ā A-test-cli-fill
+ā āāā consume ā A-test-cli-consume
+ā āāā execute ā A-test-cli-execute
+ā āāā make ā A-test-cli-make
+ā āāā gentest ā A-test-cli-gentest
+āāā exceptions/ ā A-test-exceptions
+āāā execution/ ā A-test-execution
+āāā fixtures/ ā A-test-fixtures
+āāā forks/ ā A-test-forks
+āāā pytest/ ā A-test-pytest
+āāā rpc/ ā A-test-rpc
+āāā specs/ ā A-test-specs
+āāā tools/ ā A-test-tools
+āāā types/ ā A-test-types
+āāā vm/ ā A-test-vm
+```
+
+## šØ Design Decisions
+
+### Why `A-test-*` prefix?
+
+The repo will eventually contain both **specs** and **tests**. Using `A-test-*` namespaces test framework labels, leaving room for `A-spec-*` labels in the future.
+
+### Why `F-*` for forks (not features)?
+
+Rust uses `F-*` for features, but we use it for **Ethereum forks** (Prague, Osaka, Amsterdam). This is a legitimate domain-specific deviation since forks are a core concept in Ethereum.
+
+### Why keep `good first issue`?
+
+It's a widely-recognized GitHub convention that first-time contributors actively search for. We keep it AND add `E-easy` for consistency with Rust conventions. Both can be applied to the same issue.
+
+### Why consolidate benchmark features?
+
+Three separate features (`feature:benchmark`, `feature:zkevm`, `feature:stateless`) all related to benchmarking were consolidated into a single `A-test-benchmark` label to reduce label proliferation.
+
+## š Common Tasks
+
+### Add a new label mapping
+
+Edit `scripts/label_mapping.py`:
+
+```python
+LABEL_MAPPING = {
+ # ... existing mappings ...
+ "old:label": Label("A-new-label", COLORS["area"], "Description here"),
+}
+```
+
+### Change a label's color
+
+```python
+COLORS = {
+ "area": "FBCA04", # Change this value
+ # ...
+}
+```
+
+### Add a new label group
+
+```python
+# Add to COLORS
+COLORS = {
+ # ... existing colors ...
+ "new_group": "FF0000", # Red
+}
+
+# Add labels using the new color
+LABEL_MAPPING = {
+ "something": Label("N-new-group", COLORS["new_group"], "Description"),
+}
+```
+
+### Skip the migration for a label
+
+```python
+LABEL_MAPPING = {
+ "keep-this": "KEEP", # Won't be changed
+}
+```
+
+### Delete a label
+
+```python
+LABEL_MAPPING = {
+ "delete-this": None, # Will be deleted
+}
+```
+
+## ā ļø Important Notes
+
+### Migration Mode (Step 2) - Not Yet Implemented
+
+The `migrate` mode currently:
+- ā
Creates new labels
+- ā **TODO**: Updates issues/PRs with new labels (Step 2)
+- ā
Deletes old labels
+
+**Before running migrate mode on production:**
+1. Implement Step 2 (issue/PR label updates)
+2. Test thoroughly in a fork/test repo
+3. Ensure you have backups/can revert
+
+### GitHub API Rate Limits
+
+The scripts use `gh` CLI which respects GitHub's API rate limits. For large repos with many labels/issues:
+- You may hit rate limits
+- Consider running during off-hours
+- The `gh` CLI handles auth automatically
+
+### Label Deletion
+
+Labels marked as `None` in `LABEL_MAPPING` will be **deleted** during migration. Make sure:
+- All issues/PRs are updated to use new labels first
+- You have a backup or can recreate labels if needed
+
+## š References
+
+- [Rust Label Triaging Guidelines](https://forge.rust-lang.org/release/issue-triaging.html)
+- [execution-spec-tests Issue #2142](https://github.com/ethereum/execution-spec-tests/issues/2142) - Label migration discussion
+- [execution-spec-tests Issue #2209](https://github.com/ethereum/execution-spec-tests/issues/2209) - Package restructuring
+- [ethereum/execution-specs labels](https://github.com/ethereum/execution-specs/labels) - Reference implementation
+
+## š¤ Contributing
+
+To modify the label migration:
+
+1. Edit `scripts/label_mapping.py` with your changes
+2. Test in a safe repo: `uv run python scripts/label_mapping.py create --repo YOUR_USERNAME/test-repo`
+3. Generate reports: `uv run python scripts/generate_mapping_md.py`
+4. Share for review in the relevant GitHub issue
+
+---
+
+*For questions or issues, see [#2142](https://github.com/ethereum/execution-spec-tests/issues/2142)*
diff --git a/scripts/generate_mapping_html.py b/scripts/generate_mapping_html.py
new file mode 100644
index 00000000000..59202e83c44
--- /dev/null
+++ b/scripts/generate_mapping_html.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python3
+"""Generate HTML visualization of label mappings with usage counts."""
+
+import json
+import subprocess
+from label_mapping import LABEL_MAPPING, NEW_LABELS, Label
+
+
+def run_gh(args):
+ """Run gh CLI command."""
+ result = subprocess.run(
+ ["gh"] + args, capture_output=True, text=True, check=True # noqa: S603
+ )
+ return json.loads(result.stdout) if result.stdout.strip() else []
+
+
+def get_label_usage(repo: str, label_name: str) -> tuple[int, int]:
+ """Get issue and PR count for a label."""
+ issues = run_gh([
+ "issue", "list",
+ "--repo", repo,
+ "--label", label_name,
+ "--limit", "1000",
+ "--state", "all",
+ "--json", "number"
+ ])
+ prs = run_gh([
+ "pr", "list",
+ "--repo", repo,
+ "--label", label_name,
+ "--limit", "1000",
+ "--state", "all",
+ "--json", "number"
+ ])
+ return len(issues), len(prs)
+
+
+def get_existing_label_color(repo: str, label_name: str) -> str:
+ """Get the color of an existing label."""
+ labels = run_gh([
+ "label", "list",
+ "--repo", repo,
+ "--limit", "1000",
+ "--json", "name,color"
+ ])
+ for label in labels:
+ if label["name"] == label_name:
+ return label["color"]
+ return "808080"
+
+
+def generate_html():
+ """Generate HTML visualization."""
+ repo = "ethereum/execution-spec-tests"
+
+ print("Fetching label usage data...")
+
+ html = """
+
+
+
+
+
+Label Migration Mapping
+This document shows the mapping from old labels to new labels for ethereum/execution-spec-tests
.
+
+
+
Summary:
+
+- Following Rust project label conventions
+- Labels grouped by prefix with consistent colors
+- At most one label from each group per issue/PR (flexible for multi-area issues)
+
+
+
+Label Groups
+
+- A-* (Area) - Yellow #FBCA04 - Package and area labels
+- C-* (Category) - Light Blue #C5DEF5 - Issue/PR categories
+- E-* (Experience) - Green #02E10C - Difficulty level (from Rust)
+- F-* (Fork) - Purple #D4C5F9 - Ethereum fork labels
+- P-* (Priority) - Pink #E99695 - Priority levels
+- S-* (Status) - Purple #D4C5F9 - Status labels
+
+
+Mappings
+
+
+
+Old Label |
+ |
+New Label |
+Usage (Issues/PRs) |
+
+
+
+"""
+
+ # Collect all mappings with usage data
+ mappings = []
+
+ for old_name, mapping in LABEL_MAPPING.items():
+ print(f"Processing: {old_name}")
+
+ if mapping == "KEEP":
+ continue
+
+ old_color = get_existing_label_color(repo, old_name)
+ issue_count, pr_count = get_label_usage(repo, old_name)
+ total = issue_count + pr_count
+
+ if isinstance(mapping, Label):
+ mappings.append({
+ "old_name": old_name,
+ "old_color": old_color,
+ "new_name": mapping.name,
+ "new_color": mapping.color,
+ "description": mapping.description,
+ "issues": issue_count,
+ "prs": pr_count,
+ "total": total,
+ "action": "map"
+ })
+ elif mapping is None:
+ mappings.append({
+ "old_name": old_name,
+ "old_color": old_color,
+ "new_name": "DELETE",
+ "new_color": "d73a4a",
+ "description": "Will be deleted",
+ "issues": issue_count,
+ "prs": pr_count,
+ "total": total,
+ "action": "delete"
+ })
+
+ # Sort by total usage (descending)
+ mappings.sort(key=lambda x: x["total"], reverse=True)
+
+ # Add rows
+ for m in mappings:
+ old_fg = "#000" if int(m["old_color"], 16) > 0x808080 else "#fff"
+ new_fg = "#000" if int(m["new_color"], 16) > 0x808080 else "#fff"
+
+ if m["action"] == "delete":
+ html += f"""
+{m['old_name']} |
+ā |
+DELETE |
+{m['issues']} issues, {m['prs']} PRs |
+
+"""
+ else:
+ html += f"""
+{m['old_name']} |
+ā |
+{m['new_name']} |
+{m['issues']} issues, {m['prs']} PRs |
+
+"""
+
+ # Add new labels section
+ html += """
+
+
+New Labels (No Old Label)
+
+
+
+Label |
+Description |
+
+
+
+"""
+
+ for label in sorted(NEW_LABELS, key=lambda x: x.name):
+ fg = "#000" if int(label.color, 16) > 0x808080 else "#fff"
+ html += f"""
+{label.name} |
+{label.description} |
+
+"""
+
+ html += """
+
+
+Labels Kept As-Is
+
+"""
+
+ for old_name, mapping in LABEL_MAPPING.items():
+ if mapping == "KEEP":
+ html += f"{old_name}
\n"
+
+ html += """
+
+
+
+"""
+
+ with open("mapping.html", "w") as f:
+ f.write(html)
+
+ print("\nā
Generated mapping.html")
+
+
+if __name__ == "__main__":
+ generate_html()
diff --git a/scripts/generate_mapping_md.py b/scripts/generate_mapping_md.py
new file mode 100644
index 00000000000..760425a1876
--- /dev/null
+++ b/scripts/generate_mapping_md.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+"""Generate Markdown visualization of label mappings with usage counts."""
+
+import json
+import subprocess
+from label_mapping import LABEL_MAPPING, NEW_LABELS, Label
+
+
+def run_gh(args):
+ """Run gh CLI command."""
+ result = subprocess.run(
+ ["gh"] + args, capture_output=True, text=True, check=True # noqa: S603
+ )
+ return json.loads(result.stdout) if result.stdout.strip() else []
+
+
+def get_label_usage(repo: str, label_name: str) -> tuple[int, int]:
+ """Get issue and PR count for a label."""
+ issues = run_gh([
+ "issue", "list",
+ "--repo", repo,
+ "--label", label_name,
+ "--limit", "1000",
+ "--state", "all",
+ "--json", "number"
+ ])
+ prs = run_gh([
+ "pr", "list",
+ "--repo", repo,
+ "--label", label_name,
+ "--limit", "1000",
+ "--state", "all",
+ "--json", "number"
+ ])
+ return len(issues), len(prs)
+
+
+def generate_markdown():
+ """Generate Markdown visualization."""
+ repo = "ethereum/execution-spec-tests"
+
+ print("Fetching label usage data...")
+
+ md = """# Label Migration Mapping
+
+This document shows the mapping from old labels to new labels for `ethereum/execution-spec-tests`.
+
+## Summary
+
+Following [Rust project label conventions](https://forge.rust-lang.org/release/issue-triaging.html):
+- Labels grouped by prefix with consistent colors
+- At most one label from each group per issue/PR (flexible for multi-area issues)
+
+## Label Groups
+
+| Prefix | Color | Purpose |
+|--------|-------|---------|
+| `A-*` |  `#FBCA04` Yellow | Area labels (packages, features) |
+| `C-*` |  `#C5DEF5` Light Blue | Category (bug, enhancement, etc.) |
+| `E-*` |  `#02E10C` Green | Experience/Difficulty (from Rust) |
+| `F-*` |  `#D4C5F9` Purple | Ethereum Forks |
+| `P-*` |  `#E99695` Pink | Priority |
+| `S-*` |  `#D4C5F9` Purple | Status |
+
+## Mappings
+
+| Old Label | ā | New Label | Usage (Issues/PRs) |
+|-----------|---|-----------|-------------------|
+"""
+
+ # Collect all mappings with usage data
+ mappings = []
+
+ for old_name, mapping in LABEL_MAPPING.items():
+ print(f"Processing: {old_name}")
+
+ if mapping == "KEEP":
+ continue
+
+ issue_count, pr_count = get_label_usage(repo, old_name)
+ total = issue_count + pr_count
+
+ if isinstance(mapping, Label):
+ mappings.append({
+ "old_name": old_name,
+ "new_name": mapping.name,
+ "issues": issue_count,
+ "prs": pr_count,
+ "total": total,
+ "action": "map"
+ })
+ elif mapping is None:
+ mappings.append({
+ "old_name": old_name,
+ "new_name": "~~DELETE~~",
+ "issues": issue_count,
+ "prs": pr_count,
+ "total": total,
+ "action": "delete"
+ })
+
+ # Sort by total usage (descending)
+ mappings.sort(key=lambda x: x["total"], reverse=True)
+
+ # Add rows
+ for m in mappings:
+ md += f"| `{m['old_name']}` | ā | `{m['new_name']}` | {m['issues']} issues, {m['prs']} PRs |\n"
+
+ # Add new labels section
+ md += """
+## New Labels (No Old Label Mapping)
+
+| Label | Description |
+|-------|-------------|
+"""
+
+ for label in sorted(NEW_LABELS, key=lambda x: x.name):
+ md += f"| `{label.name}` | {label.description} |\n"
+
+ md += """
+## Labels Kept As-Is
+
+These labels remain unchanged:
+
+"""
+
+ for old_name, mapping in sorted(LABEL_MAPPING.items()):
+ if mapping == "KEEP":
+ md += f"- `{old_name}`\n"
+
+ md += """
+---
+
+*Generated by `scripts/generate_mapping_md.py`*
+"""
+
+ with open("mapping.md", "w") as f:
+ f.write(md)
+
+ print("\nā
Generated mapping.md")
+
+
+if __name__ == "__main__":
+ generate_markdown()
diff --git a/scripts/label_mapping.py b/scripts/label_mapping.py
new file mode 100644
index 00000000000..c7de3ae8a68
--- /dev/null
+++ b/scripts/label_mapping.py
@@ -0,0 +1,400 @@
+#!/usr/bin/env python3
+"""
+Label migration script for ethereum/execution-spec-tests.
+
+This script helps migrate labels to align with execution-specs conventions
+and the new package structure from issue #2209.
+
+Modes:
+ 1. create: Create new labels in target repo (safe, for testing)
+ 2. migrate: Full migration - create labels, update issues/PRs, delete old labels
+
+Usage:
+ # Test label creation in a different repo
+ python scripts/label_mapping.py create --repo danceratopz/execution-specs-labels
+
+ # Full migration (DESTRUCTIVE - use with caution)
+ python scripts/label_mapping.py migrate --repo ethereum/execution-spec-tests
+"""
+
+import argparse
+import json
+import subprocess
+import sys
+from dataclasses import dataclass
+from typing import Any
+
+
+@dataclass
+class Label:
+ """Represents a GitHub label."""
+
+ name: str
+ color: str
+ description: str
+
+ def to_dict(self) -> dict[str, str]:
+ """Convert to dictionary for JSON serialization."""
+ return {"name": self.name, "color": self.color, "description": self.description}
+
+
+# Color scheme aligned with ethereum/execution-specs
+COLORS = {
+ "area": "FBCA04", # Yellow - Area labels (A-*)
+ "category": "C5DEF5", # Light Blue - Category labels (C-*)
+ "experience": "02E10C", # Bright Green - Experience/Difficulty labels (E-*, from Rust)
+ "priority": "E99695", # Pink - Priority labels (P-*)
+ "fork": "D4C5F9", # Purple - Fork labels (F-*)
+ "status": "D4C5F9", # Purple - Status labels (S-*)
+}
+
+
+# Label mapping configuration
+# Format: "old_label_name": Label(new_name, color, description)
+# Set to None to delete a label without replacement
+# Set to "KEEP" to keep the label unchanged
+LABEL_MAPPING: dict[str, Label | None | str] = {
+ # Area: Package-level labels (A-test-*)
+ "scope:base_types": Label(
+ "A-test-base_types",
+ COLORS["area"],
+ "Area: ethereum_spec_tests/base_types package",
+ ),
+ "scope:types": Label(
+ "A-test-types", COLORS["area"], "Area: ethereum_spec_tests/types package"
+ ),
+ "scope:tools": Label(
+ "A-test-tools", COLORS["area"], "Area: ethereum_spec_tests/tools package"
+ ),
+ "scope:forks": Label(
+ "A-test-forks", COLORS["area"], "Area: ethereum_spec_tests/forks package"
+ ),
+ "scope:pytest": Label(
+ "A-test-pytest", COLORS["area"], "Area: ethereum_spec_tests/pytest package"
+ ),
+ "scope:exceptions": Label(
+ "A-test-exceptions",
+ COLORS["area"],
+ "Area: ethereum_spec_tests/exceptions package",
+ ),
+ "scope:cli": Label(
+ "A-test-client-clis",
+ COLORS["area"],
+ "Area: ethereum_spec_tests/client_clis package",
+ ),
+ "scope:tests": Label(
+ "A-test-tests", COLORS["area"], "Area: EL client test cases in ./tests/"
+ ),
+ "scope:docs": Label("A-doc", COLORS["area"], "Area: documentation"),
+ "scope:ci": Label("A-ci", COLORS["area"], "Area: continuous integration"),
+ "scope:tooling": Label(
+ "A-test-config", COLORS["area"], "Area: Python tooling configuration (uv, ruff, tox)"
+ ),
+ "scope:deps": Label(
+ "A-test-deps", COLORS["area"], "Area: package dependency updates"
+ ),
+ "scope:packaging": Label(
+ "A-test-packaging", COLORS["area"], "Area: Python packaging configuration"
+ ),
+ # Area: CLI sub-commands (A-test-cli-*)
+ "scope:eest": Label(
+ "A-test-cli-eest", COLORS["area"], "Area: ethereum_test_cli/eest command"
+ ),
+ "scope:make": Label(
+ "A-test-cli-make", COLORS["area"], "Area: ethereum_test_cli/make command"
+ ),
+ "scope:gentest": Label(
+ "A-test-cli-gentest", COLORS["area"], "Area: ethereum_test_cli/gentest command"
+ ),
+ "scope:fill": Label(
+ "A-test-cli-fill", COLORS["area"], "Area: ethereum_test_cli/fill command"
+ ),
+ "scope:consume": Label(
+ "A-test-cli-consume", COLORS["area"], "Area: ethereum_test_cli/consume command"
+ ),
+ "scope:execute": Label(
+ "A-test-cli-execute", COLORS["area"], "Area: ethereum_test_cli/execute command"
+ ),
+ # Delete vague labels
+ "scope:fw": None, # Too vague - users should use specific package labels
+ "scope:checklists": None, # Package being removed/integrated
+ "scope:evm": Label(
+ "A-test-vm", COLORS["area"], "Area: ethereum_spec_tests/vm package"
+ ),
+ # Category labels (C-*)
+ "type:bug": Label("C-bug", COLORS["category"], "Category: bug, deviation, or problem"),
+ "type:feat": Label(
+ "C-enhance", COLORS["category"], "Category: request for an improvement"
+ ),
+ "type:refactor": Label(
+ "C-refactor", COLORS["category"], "Category: code refactoring"
+ ),
+ "type:test": Label(
+ "C-test", COLORS["category"], "Category: framework unit tests (not EL client tests)"
+ ),
+ "type:chore": Label("C-chore", COLORS["category"], "Category: maintenance task"),
+ "type:docs": Label(
+ "C-doc", COLORS["category"], "Category: documentation improvement"
+ ),
+ # Fork labels (F-*)
+ "fork:prague": Label("F-prague", COLORS["fork"], "Fork: Prague hardfork"),
+ "fork:osaka": Label("F-osaka", COLORS["fork"], "Fork: Osaka hardfork"),
+ "fork:amsterdam": Label("F-amsterdam", COLORS["fork"], "Fork: Amsterdam hardfork"),
+ "fork: amsterdam": Label(
+ "F-amsterdam", COLORS["fork"], "Fork: Amsterdam hardfork"
+ ), # Fix spacing
+ # Status labels (S-*)
+ "needs-discussion": Label(
+ "S-needs-discussion", COLORS["status"], "Status: needs discussion before proceeding"
+ ),
+ "needs-attention": Label(
+ "S-needs-attention", COLORS["status"], "Status: needs attention from maintainers"
+ ),
+ # Keep these unchanged (standard GitHub labels)
+ "help wanted": "KEEP",
+ "good first issue": Label(
+ "good first issue", COLORS["experience"], "Good for newcomers"
+ ),
+ "tracker": "KEEP",
+ "automated issue": "KEEP",
+ "report": "KEEP",
+ "port": "KEEP",
+ "t8ntools": Label(
+ "A-test-client-clis",
+ COLORS["area"],
+ "Area: ethereum_spec_tests/client_clis package",
+ ),
+ "feature:eof": Label(
+ "A-test-eof", COLORS["area"], "Area: EOF (EVM Object Format) feature"
+ ),
+ "feature:stateless": Label(
+ "A-test-benchmark", COLORS["area"], "Area: Benchmarking feature"
+ ),
+ "feature:zkevm": Label(
+ "A-test-benchmark", COLORS["area"], "Area: Benchmarking feature"
+ ),
+ "feature:benchmark": Label(
+ "A-test-benchmark", COLORS["area"], "Area: Benchmarking feature"
+ ),
+ "Finalize Weld": "KEEP",
+ # Priority labels already aligned
+ "P-low": "KEEP",
+ "P-medium": "KEEP",
+ "P-high": "KEEP",
+ "P-urgent": "KEEP",
+ # Delete these generic/duplicate labels
+ "duplicate": None,
+ "invalid": None,
+ "question": Label(
+ "C-question",
+ COLORS["category"],
+ "Category: question or request for clarification",
+ ),
+ "wontfix": None,
+}
+
+
+# New labels to create (not mapped from old ones)
+NEW_LABELS: list[Label] = [
+ Label(
+ "A-test-ethereum_test_cli",
+ COLORS["area"],
+ "Area: ethereum_spec_tests/ethereum_test_cli package",
+ ),
+ Label(
+ "A-test-execution", COLORS["area"], "Area: ethereum_spec_tests/execution package"
+ ),
+ Label(
+ "A-test-fixtures", COLORS["area"], "Area: ethereum_spec_tests/fixtures package"
+ ),
+ Label("A-test-rpc", COLORS["area"], "Area: ethereum_spec_tests/rpc package"),
+ Label("A-test-specs", COLORS["area"], "Area: ethereum_spec_tests/specs package"),
+ # Experience labels to align with execution-specs
+ Label("E-easy", COLORS["experience"], "Experience: easy, good for newcomers"),
+ Label("E-medium", COLORS["experience"], "Experience: of moderate difficulty"),
+ Label(
+ "E-hard",
+ COLORS["experience"],
+ "Experience: difficult, probably not for the faint of heart",
+ ),
+]
+
+
+def run_gh_command(args: list[str]) -> dict[str, Any] | list[dict[str, Any]] | str:
+ """Run a gh CLI command and return parsed JSON output."""
+ result = subprocess.run(
+ ["gh"] + args, capture_output=True, text=True, check=True # noqa: S603
+ )
+ if result.stdout.strip():
+ return json.loads(result.stdout)
+ return result.stdout
+
+
+def fetch_labels(repo: str) -> list[Label]:
+ """Fetch all labels from a GitHub repository."""
+ print(f"Fetching labels from {repo}...")
+ data = run_gh_command(
+ ["label", "list", "--repo", repo, "--limit", "1000", "--json", "name,color,description"]
+ )
+ assert isinstance(data, list), "Expected list of labels"
+ return [Label(name=label["name"], color=label["color"], description=label["description"]) for label in data]
+
+
+def create_label(repo: str, label: Label, dry_run: bool = False) -> None:
+ """Create a new label in the repository."""
+ cmd = f"Creating label: {label.name}"
+ if dry_run:
+ print(f"[DRY RUN] {cmd}")
+ return
+
+ print(cmd)
+ try:
+ run_gh_command(
+ [
+ "label",
+ "create",
+ label.name,
+ "--repo",
+ repo,
+ "--color",
+ label.color,
+ "--description",
+ label.description,
+ ]
+ )
+ except subprocess.CalledProcessError as e:
+ if "already exists" in e.stderr:
+ print(f" ā ļø Label already exists, skipping")
+ else:
+ raise
+
+
+def delete_label(repo: str, label_name: str, dry_run: bool = False) -> None:
+ """Delete a label from the repository."""
+ cmd = f"Deleting label: {label_name}"
+ if dry_run:
+ print(f"[DRY RUN] {cmd}")
+ return
+
+ print(cmd)
+ run_gh_command(["label", "delete", label_name, "--repo", repo, "--yes"])
+
+
+def create_mode(repo: str, dry_run: bool = False) -> None:
+ """Mode 1: Create new labels in target repo (safe mode)."""
+ print(f"\n{'='*80}")
+ print(f"CREATE MODE: Creating new labels in {repo}")
+ print(f"{'='*80}\n")
+
+ # Fetch existing labels
+ existing_labels = fetch_labels(repo)
+ existing_names = {label.name for label in existing_labels}
+
+ # Collect all new labels to create
+ labels_to_create: list[Label] = []
+
+ # Add mapped labels
+ for old_name, mapping in LABEL_MAPPING.items():
+ if isinstance(mapping, Label):
+ if mapping.name not in existing_names:
+ labels_to_create.append(mapping)
+
+ # Add new labels
+ for label in NEW_LABELS:
+ if label.name not in existing_names:
+ labels_to_create.append(label)
+
+ # Remove duplicates
+ seen = set()
+ unique_labels = []
+ for label in labels_to_create:
+ if label.name not in seen:
+ seen.add(label.name)
+ unique_labels.append(label)
+
+ print(f"\nWill create {len(unique_labels)} new labels:\n")
+ for label in sorted(unique_labels, key=lambda x: x.name):
+ print(f" ⢠{label.name:40s} #{label.color} - {label.description}")
+
+ if not dry_run:
+ print("\nCreating labels...")
+ for label in unique_labels:
+ create_label(repo, label, dry_run=dry_run)
+
+ print(f"\nā
Done! Created {len(unique_labels)} labels in {repo}")
+
+
+def migrate_mode(repo: str, dry_run: bool = False) -> None:
+ """Mode 2: Full migration - create labels, update issues/PRs, delete old labels."""
+ print(f"\n{'='*80}")
+ print(f"MIGRATE MODE: Full label migration for {repo}")
+ print(f"{'='*80}\n")
+
+ if not dry_run:
+ response = input("ā ļø This will modify labels on ALL issues and PRs. Continue? [y/N] ")
+ if response.lower() != "y":
+ print("Migration cancelled.")
+ sys.exit(0)
+
+ # Step 1: Create all new labels
+ print("\n[Step 1/3] Creating new labels...")
+ create_mode(repo, dry_run=dry_run)
+
+ # Step 2: Update issues and PRs
+ print("\n[Step 2/3] Updating issues and PRs...")
+ # TODO: Implement issue/PR label updates
+
+ # Step 3: Delete old labels
+ print("\n[Step 3/3] Deleting old labels...")
+ labels_to_delete = [
+ old_name for old_name, mapping in LABEL_MAPPING.items() if mapping is None
+ ]
+
+ for label_name in labels_to_delete:
+ delete_label(repo, label_name, dry_run=dry_run)
+
+ print(f"\nā
Migration complete for {repo}")
+
+
+def main() -> None:
+ """Main entry point."""
+ parser = argparse.ArgumentParser(
+ description="Migrate labels for execution-spec-tests",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=__doc__,
+ )
+ parser.add_argument(
+ "mode",
+ choices=["create", "migrate"],
+ help="Mode: 'create' (safe, create labels only) or 'migrate' (full migration)",
+ )
+ parser.add_argument(
+ "--repo",
+ required=True,
+ help="Target repository (e.g., 'danceratopz/execution-specs-labels')",
+ )
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ help="Show what would be done without making changes",
+ )
+
+ args = parser.parse_args()
+
+ try:
+ if args.mode == "create":
+ create_mode(args.repo, dry_run=args.dry_run)
+ elif args.mode == "migrate":
+ migrate_mode(args.repo, dry_run=args.dry_run)
+ except subprocess.CalledProcessError as e:
+ print(f"\nā Error running gh command: {e}", file=sys.stderr)
+ if e.stderr:
+ print(f"stderr: {e.stderr}", file=sys.stderr)
+ sys.exit(1)
+ except KeyboardInterrupt:
+ print("\n\nCancelled by user.")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/label_usage.py b/scripts/label_usage.py
new file mode 100644
index 00000000000..ccc9ae53cf9
--- /dev/null
+++ b/scripts/label_usage.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+"""Check label usage in ethereum/execution-spec-tests."""
+
+import json
+import subprocess
+
+def run_gh(args):
+ """Run gh CLI command."""
+ result = subprocess.run(
+ ["gh"] + args, capture_output=True, text=True, check=True
+ )
+ return json.loads(result.stdout)
+
+# Get all labels
+labels = run_gh([
+ "label", "list",
+ "--repo", "ethereum/execution-spec-tests",
+ "--limit", "1000",
+ "--json", "name,color,description"
+])
+
+print("Counting label usage...")
+usage = []
+
+for label in labels:
+ name = label["name"]
+
+ # Count issues
+ issues = run_gh([
+ "issue", "list",
+ "--repo", "ethereum/execution-spec-tests",
+ "--label", name,
+ "--limit", "1000",
+ "--state", "all",
+ "--json", "number"
+ ])
+
+ # Count PRs
+ prs = run_gh([
+ "pr", "list",
+ "--repo", "ethereum/execution-spec-tests",
+ "--label", name,
+ "--limit", "1000",
+ "--state", "all",
+ "--json", "number"
+ ])
+
+ total = len(issues) + len(prs)
+ usage.append((total, name))
+ print(f"{name}: {total}")
+
+# Sort by usage
+usage.sort()
+
+print("\n" + "="*80)
+print("Labels with < 5 uses:")
+print("="*80)
+for count, name in usage:
+ if count < 5:
+ print(f"{count:3d} | {name}")
+ else:
+ break
+
+print("\n" + "="*80)
+print("All labels sorted by usage:")
+print("="*80)
+for count, name in usage:
+ print(f"{count:3d} | {name}")