diff --git a/.github/label-pr.json b/.github/label-pr.json index 931eba5973..e32a7ed30b 100644 --- a/.github/label-pr.json +++ b/.github/label-pr.json @@ -1,5 +1,4 @@ { - "catch_all_label": "t:misc", "title_patterns": { "t:feature-app": ["feat", "feature"], "t:feature-tool": ["tool"], diff --git a/.github/scripts/label-pr.py b/.github/scripts/label-pr.py index 9e110e4144..4e803df447 100644 --- a/.github/scripts/label-pr.py +++ b/.github/scripts/label-pr.py @@ -4,21 +4,22 @@ Label pull requests based on changed file paths and PR title patterns (conventional commit format). Usage: - python label-pr.py [-a|--add|-r|--replace] [-d|--dry-run] [-c|--config CONFIG] + python label-pr.py [-a|--add|-r|--replace] [-d|--dry-run] [-c|--config CONFIG] Arguments: pr-number: The pull request number + pr-labels: Current PR labels as JSON array string -a, --add: Add labels without removing existing ones (default) -r, --replace: Replace all existing labels -d, --dry-run: Run without actually applying labels -c, --config: Path to JSON config file (default: .github/label-pr.json) Examples: - python label-pr.py 1234 - python label-pr.py 1234 -a - python label-pr.py 1234 --replace - python label-pr.py 1234 -r -d - python label-pr.py 1234 --config custom-config.json + python label-pr.py 1234 '[]' + python label-pr.py 1234 '[{"name":"label1"}]' -a + python label-pr.py 1234 '[{"name":"label1"}]' --replace + python label-pr.py 1234 '[{"name":"label1"}]' -r -d + python label-pr.py 1234 '[]' --config custom-config.json """ import argparse @@ -42,9 +43,6 @@ def load_config_json(config_file: str) -> dict: print(f"✅ Loaded config from: {config_file}") valid_config = True - if not config.get("catch_all_label"): - print("❌ Missing 'catch_all_label' in config file") - valid_config = False if not config.get("title_patterns"): print("❌ Missing 'title_patterns' in config file") valid_config = False @@ -155,6 +153,27 @@ def label_title(pr_title: str, title_patterns: dict) -> list[str]: return list(labels_to_apply) +def parse_pr_labels(pr_labels_str: str) -> list[str]: + """Parse PR labels from JSON array string.""" + try: + labels = json.loads(pr_labels_str) + if not isinstance(labels, list): + print("::warning::Failed to parse PR labels: not a list") + return [] + return [item.get("name") for item in labels if item.get("name")] + except (json.JSONDecodeError, TypeError) as e: + print(f"::error::Error parsing PR labels: {e}") + return [] + +def get_preserved_labels(pr_labels_str: str) -> list[str]: + """Get existing PR labels that should be preserved (exclude app: and t: labels).""" + existing_labels = parse_pr_labels(pr_labels_str) + print(f"🔍 Parsed PR labels: {existing_labels}") + preserved_labels = [label for label in existing_labels if not (label.startswith("app:") or label.startswith("t:"))] + if preserved_labels: + print(f"🔍 Preserving existing labels: {', '.join(preserved_labels)}") + return preserved_labels + def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser( @@ -165,6 +184,11 @@ def parse_args(): help="The pull request number" ) + parser.add_argument( + "pr_labels", + help="Current PR labels (JSON array)" + ) + mode_group = parser.add_mutually_exclusive_group() mode_group.add_argument( "-a", "--add", @@ -194,7 +218,6 @@ def parse_args(): def main(): args = parse_args() config = load_config_json(args.config) - CATCH_ALL_LABEL = config["catch_all_label"] LABEL_TITLE_PATTERNS = config["title_patterns"] LABEL_PATH_PATTERNS = config["path_patterns"] @@ -216,16 +239,18 @@ def main(): title_labels = label_title(pr_title, LABEL_TITLE_PATTERNS) all_labels = set(filepath_labels + title_labels) - if not any(label.startswith("t:") for label in all_labels): - all_labels.add(CATCH_ALL_LABEL) - if all_labels: + print("--------------------------------") labels_str = ', '.join(sorted(all_labels)) if mode == "add": print(f"::notice::🏷️ Adding labels: {labels_str}") if not args.dry_run: gh_add_labels(pr_number, list(all_labels)) else: + preserved_labels = get_preserved_labels(args.pr_labels) + if preserved_labels: + all_labels.update(preserved_labels) + labels_str = ', '.join(sorted(all_labels)) print(f"::notice::🏷️ Replacing labels with: {labels_str}") if not args.dry_run: gh_replace_labels(pr_number, list(all_labels)) diff --git a/.github/workflows/sdlc-label-pr.yml b/.github/workflows/sdlc-label-pr.yml index de2fdc187d..80a3e98143 100644 --- a/.github/workflows/sdlc-label-pr.yml +++ b/.github/workflows/sdlc-label-pr.yml @@ -1,5 +1,5 @@ -name: SDLC / Label PR by Files - +name: SDLC / Label PR +run-name: Label PR ${{ github.event.pull_request.number || inputs.pr-number }}${{ github.event_name == 'workflow_dispatch' && format(' / mode "{0}" dry-run "{1}"', inputs.mode, inputs.dry-run) || '' }} on: pull_request: types: [opened, synchronize] @@ -63,7 +63,7 @@ jobs: exit 0 fi - if [ "$_PR_USER" = "renovate[bot]" ] || [ "$_PR_USER" = "bw-ghapp[bot]" ]; then + if [[ "$_PR_USER" == app/* || "$_PR_USER" == *\[bot\] ]]; then echo "➡️ Bot PR ($_PR_USER). Label mode: --add" echo "label_mode=--add" >> "$GITHUB_OUTPUT" exit 0 @@ -77,7 +77,14 @@ jobs: GH_TOKEN: ${{ github.token }} _LABEL_MODE: ${{ inputs.mode && format('--{0}', inputs.mode) || steps.label-mode.outputs.label_mode }} _DRY_RUN: ${{ inputs.dry-run == true && '--dry-run' || '' }} + _PR_LABELS: ${{ toJSON(github.event.pull_request.labels) }} run: | - echo "🔍 Labeling PR #$_PR_NUMBER with mode: $_LABEL_MODE and dry-run: $_DRY_RUN" - python3 .github/scripts/label-pr.py "$_PR_NUMBER" "$_LABEL_MODE" "$_DRY_RUN" + if [ -z "$_PR_LABELS" ] || [ "$_PR_LABELS" = "null" ] || [ "$_PR_LABELS" = "[]" ]; then + echo "🔍 No current PR labels found, retrieving PR data for PR #$_PR_NUMBER..." + _PR_LABELS=$(gh pr view "$_PR_NUMBER" --json labels --jq '.labels') + fi + echo "🔍 Labeling PR #$_PR_NUMBER with mode: \"$_LABEL_MODE\" and dry-run: \"$_DRY_RUN\" and current PR labels: \"$_PR_LABELS\"..." + echo "🐍 Running label-pr.py script..." + echo "" + python3 .github/scripts/label-pr.py "$_PR_NUMBER" "$_PR_LABELS" "$_LABEL_MODE" "$_DRY_RUN"