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](https://via.placeholder.com/15/FBCA04/000000?text=+) `#FBCA04` Yellow | Area (packages, features) | `A-test-tools`, `A-test-cli-fill` | +| `C-*` | ![#C5DEF5](https://via.placeholder.com/15/C5DEF5/000000?text=+) `#C5DEF5` Light Blue | Category (bug, enhancement, etc.) | `C-bug`, `C-enhance` | +| `E-*` | ![#02E10C](https://via.placeholder.com/15/02E10C/000000?text=+) `#02E10C` Bright Green | Experience/Difficulty | `E-easy`, `E-medium`, `E-hard` | +| `F-*` | ![#D4C5F9](https://via.placeholder.com/15/D4C5F9/000000?text=+) `#D4C5F9` Purple | Ethereum Forks | `F-prague`, `F-osaka` | +| `P-*` | ![#E99695](https://via.placeholder.com/15/E99695/000000?text=+) `#E99695` Pink | Priority | `P-low`, `P-medium`, `P-high`, `P-urgent` | +| `S-*` | ![#D4C5F9](https://via.placeholder.com/15/D4C5F9/000000?text=+) `#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: + +
+ +

Label Groups

+ + +

Mappings

+ + + + + + + + + + +""" + + # 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""" + + + + + +""" + else: + html += f""" + + + + + +""" + + # Add new labels section + html += """ +
Old LabelNew LabelUsage (Issues/PRs)
{m['old_name']}→DELETE{m['issues']} issues, {m['prs']} PRs
{m['old_name']}→{m['new_name']}{m['issues']} issues, {m['prs']} PRs
+ +

New Labels (No Old Label)

+ + + + + + + + +""" + + for label in sorted(NEW_LABELS, key=lambda x: x.name): + fg = "#000" if int(label.color, 16) > 0x808080 else "#fff" + html += f""" + + + +""" + + html += """ +
LabelDescription
{label.name}{label.description}
+ +

Labels Kept As-Is

+ + + + +""" + + 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](https://via.placeholder.com/15/FBCA04/000000?text=+) `#FBCA04` Yellow | Area labels (packages, features) | +| `C-*` | ![#C5DEF5](https://via.placeholder.com/15/C5DEF5/000000?text=+) `#C5DEF5` Light Blue | Category (bug, enhancement, etc.) | +| `E-*` | ![#02E10C](https://via.placeholder.com/15/02E10C/000000?text=+) `#02E10C` Green | Experience/Difficulty (from Rust) | +| `F-*` | ![#D4C5F9](https://via.placeholder.com/15/D4C5F9/000000?text=+) `#D4C5F9` Purple | Ethereum Forks | +| `P-*` | ![#E99695](https://via.placeholder.com/15/E99695/000000?text=+) `#E99695` Pink | Priority | +| `S-*` | ![#D4C5F9](https://via.placeholder.com/15/D4C5F9/000000?text=+) `#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}")