From 1dec3aee715afc05c48de2c630a2d0f7cbd85241 Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Mon, 22 Jun 2026 21:12:55 +0200 Subject: [PATCH 1/5] Create composite action to label PR by change type via title or changed files --- auto-label-pr-sdlc/README.md | 108 +++++++++++++ auto-label-pr-sdlc/action.yml | 56 +++++++ auto-label-pr-sdlc/label-pr.py | 288 +++++++++++++++++++++++++++++++++ 3 files changed, 452 insertions(+) create mode 100644 auto-label-pr-sdlc/README.md create mode 100644 auto-label-pr-sdlc/action.yml create mode 100644 auto-label-pr-sdlc/label-pr.py diff --git a/auto-label-pr-sdlc/README.md b/auto-label-pr-sdlc/README.md new file mode 100644 index 000000000..df2b57a8e --- /dev/null +++ b/auto-label-pr-sdlc/README.md @@ -0,0 +1,108 @@ +# SDLC / Auto Label PR Composite Action + +## Description + +The `auto-label-pr-sdlc` composite action labels pull requests based on changed file paths and PR title patterns (conventional commit format). Each repository provides its own `label-pr.json` config defining which labels to apply for which file paths or title prefixes. + +## Key Features + +- **Title-based labeling**: Matches conventional commit prefixes (e.g. `feat:`, `fix(scope):`) to `t:*` labels +- **Path-based labeling**: Matches changed file paths against configurable prefix patterns +- **Negation patterns**: Supports `.gitignore`-style `!` exclusion patterns within path rules +- **Add or replace mode**: Either appends labels or does a full replace (preserving non-`t:`/`app:` labels like `hold`, `needs-qa`) +- **Dry-run support**: Preview what labels would be applied without mutating the PR + +## How to use it + +### 1. Add a config file to your repository + +Create `.github/label-pr.json` in your repository: + +```json +{ + "title_patterns": { + "t:feature": ["feat", "feature"], + "t:bugfix": ["fix", "bug"], + "t:tech-debt": ["refactor", "chore", "cleanup"], + "t:docs": ["docs"], + "t:ci": ["ci", "build"], + "t:deps": ["deps"] + }, + "path_patterns": { + "t:ci": [".github/", "scripts/"], + "t:docs": ["docs/", "README.md"] + } +} +``` + +Both `title_patterns` and `path_patterns` keys are required. + +**Title matching**: checks for `:` or `(` anywhere in the lowercased PR title (conventional commit format). + +**Path matching**: checks if any changed file path starts with the pattern string. Prefix a pattern with `!` to exclude previously matched files (last match wins, like `.gitignore`). + +### 2. Add a workflow to your repository + +```yaml +name: Label PR +run-name: Label PR #${{ github.event.pull_request.number }} + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: {} + +jobs: + label: + name: Label PR + runs-on: ubuntu-24.04 + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Label PR + uses: bitwarden/gh-actions/auto-label-pr-sdlc@main + with: + pr-number: ${{ github.event.pull_request.number }} + pr-labels: ${{ toJSON(github.event.pull_request.labels) }} + mode: replace + github-token: ${{ secrets.GITHUB_TOKEN }} +``` + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `pr-number` | Yes | — | The pull request number | +| `pr-labels` | No | `[]` | Current PR labels as JSON array (e.g. `toJSON(github.event.pull_request.labels)`) | +| `mode` | No | `add` | `add` appends labels; `replace` rewrites `t:`/`app:` labels while preserving others | +| `dry-run` | No | `false` | Set to `true` to preview without applying | +| `config-path` | No | `.github/label-pr.json` | Path to config JSON, relative to workspace root | +| `github-token` | Yes | — | GitHub token with `pull-requests: write` permission | + +## Mode behavior + +- **`add`**: Calls `gh pr edit --add-label`. Existing labels are never removed. +- **`replace`**: Calls `PATCH /issues/:number` to set the exact label list. Labels that don't start with `t:` or `app:` (e.g. `hold`, `needs-qa`, `DB-migrations-changed`) are preserved automatically. + +## Requirements + +- The repository must have the relevant labels created (e.g. `t:feature`, `t:bugfix`). +- The runner must have Python 3.9+ and the `gh` CLI available (both are present on all GitHub-hosted runners). + +## Troubleshooting + +### No labels applied + +- Check that `label-pr.json` has both `title_patterns` and `path_patterns` keys. +- Verify the labels referenced in the config exist in the repository. +- Run with `dry-run: 'true'` to see what would be matched without applying. + +### Config file not found + +- Ensure `actions/checkout` runs before this action so the workspace is populated. +- The `config-path` is relative to the workspace root; the default is `.github/label-pr.json`. diff --git a/auto-label-pr-sdlc/action.yml b/auto-label-pr-sdlc/action.yml new file mode 100644 index 000000000..04167f820 --- /dev/null +++ b/auto-label-pr-sdlc/action.yml @@ -0,0 +1,56 @@ +name: "SDLC / Auto Label PR" +description: "Label pull requests based on changed file paths and PR title patterns (conventional commit format)" +author: "Bitwarden" +branding: + icon: tag + color: blue + +inputs: + pr-number: + description: "The pull request number" + required: true + pr-labels: + description: "Current PR labels as JSON array string (e.g. '[{\"name\":\"label1\"}]')" + required: false + default: "[]" + mode: + description: "'add' (default) adds labels without removing existing ones; 'replace' rewrites t:/app: labels while preserving others (e.g. hold, needs-qa)" + required: false + default: "add" + dry-run: + description: "Set to 'true' to run without applying labels" + required: false + default: "false" + config-path: + description: "Path to label config JSON file, relative to the workspace root (default: .github/label-pr.json)" + required: false + default: ".github/label-pr.json" + github-token: + description: "GitHub token with pull-requests:write permission" + required: true + +runs: + using: "composite" + steps: + - name: Label PR + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + PR_NUMBER: ${{ inputs.pr-number }} + PR_LABELS: ${{ inputs.pr-labels }} + MODE: ${{ inputs.mode }} + DRY_RUN: ${{ inputs.dry-run }} + CONFIG_PATH: ${{ inputs.config-path }} + run: | + MODE_FLAG="" + [[ "$MODE" == "replace" ]] && MODE_FLAG="--replace" + + DRY_RUN_FLAG="" + [[ "$DRY_RUN" == "true" ]] && DRY_RUN_FLAG="--dry-run" + + python3 "${{ github.action_path }}/label-pr.py" \ + "$PR_NUMBER" \ + "$PR_LABELS" \ + $MODE_FLAG \ + $DRY_RUN_FLAG \ + --config "$CONFIG_PATH" diff --git a/auto-label-pr-sdlc/label-pr.py b/auto-label-pr-sdlc/label-pr.py new file mode 100644 index 000000000..063113e63 --- /dev/null +++ b/auto-label-pr-sdlc/label-pr.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +# Requires Python 3.9+ +""" +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] + +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 '[{"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 +import json +import os +import subprocess +import sys + +DEFAULT_MODE = "add" +DEFAULT_CONFIG_PATH = ".github/label-pr.json" + +def load_config_json(config_file: str) -> dict: + """Load configuration from JSON file.""" + if not os.path.exists(config_file): + print(f"\u274c Config file not found: {config_file}") + sys.exit(1) + + try: + with open(config_file, "r") as f: + config = json.load(f) + print(f"\u2705 Loaded config from: {config_file}") + + valid_config = True + if not config.get("title_patterns"): + print("\u274c Missing 'title_patterns' in config file") + valid_config = False + if not config.get("path_patterns"): + print("\u274c Missing 'path_patterns' in config file") + valid_config = False + + if not valid_config: + print("::error::Invalid label-pr.json config file, exiting...") + sys.exit(1) + + return config + except json.JSONDecodeError as e: + print(f"\u274c JSON deserialization error in label-pr.json config: {e}") + sys.exit(1) + except Exception as e: + print(f"\u274c Unexpected error loading label-pr.json config: {e}") + sys.exit(1) + +def gh_get_changed_files(pr_number: str) -> list[str]: + """Get list of changed files in a pull request.""" + try: + result = subprocess.run( + ["gh", "pr", "diff", pr_number, "--name-only"], + capture_output=True, + text=True, + check=True, + ) + changed_files = result.stdout.strip().split("\n") + return list(filter(None, changed_files)) + except subprocess.CalledProcessError as e: + print(f"::error::Error getting changed files: {e}") + return [] + +def gh_get_pr_title(pr_number: str) -> str: + """Get the title of a pull request.""" + try: + result = subprocess.run( + ["gh", "pr", "view", pr_number, "--json", "title", "--jq", ".title"], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"::error::Error getting PR title: {e}") + return "" + +def gh_add_labels(pr_number: str, labels: list[str]) -> None: + """Add labels to a pull request (doesn't remove existing labels).""" + gh_labels = ",".join(labels) + subprocess.run( + ["gh", "pr", "edit", pr_number, "--add-label", gh_labels], + check=True, + ) + +def gh_replace_labels(pr_number: str, labels: list[str]) -> None: + """Replace all labels on a pull request with the specified labels.""" + payload = json.dumps({"labels": labels}) + subprocess.run( + ["gh", "api", "repos/{owner}/{repo}/issues/" + pr_number, "-X", "PATCH", "--silent", "--input", "-"], + input=payload, + text=True, + check=True, + ) + +def file_matches_patterns(file: str, patterns: list[str]) -> bool: + """Check if a file matches the pattern list using .gitignore-style negation. + + Patterns are evaluated in order: + - Regular patterns (e.g. "libs/") include files starting with that prefix. + - Negation patterns (e.g. "!libs/angular/") exclude previously matched files. + + The last matching pattern wins, just like .gitignore. + """ + matched = False + for pattern in patterns: + if pattern.startswith("!"): + if file.startswith(pattern[1:]): + matched = False + else: + if file.startswith(pattern): + matched = True + return matched + +def label_filepaths(changed_files: list[str], path_patterns: dict) -> list[str]: + """Check changed files against path patterns and return labels to apply.""" + if not changed_files: + return [] + + labels_to_apply = set() # Use set to avoid duplicates + + for label, patterns in path_patterns.items(): + for file in changed_files: + if file_matches_patterns(file, patterns): + print(f"\U0001f436 File '{file}' matches pattern for label '{label}'") + labels_to_apply.add(label) + break + + if "app:shared" in labels_to_apply: + labels_to_apply.add("app:password-manager") + labels_to_apply.add("app:authenticator") + labels_to_apply.remove("app:shared") + + if not labels_to_apply: + print("::notice::No matching file paths found.") + + return list(labels_to_apply) + +def label_title(pr_title: str, title_patterns: dict) -> list[str]: + """Check PR title against patterns and return labels to apply.""" + if not pr_title: + return [] + + labels_to_apply = set() + title_lower = pr_title.lower() + for label, patterns in title_patterns.items(): + for pattern in patterns: + # Check for pattern with : or ( suffix (conventional commits format) + if f"{pattern}:" in title_lower or f"{pattern}(" in title_lower: + print(f"\U0001f4cb Title matches pattern '{pattern}' for label '{label}'") + labels_to_apply.add(label) + break + + if not labels_to_apply: + print("::notice::No matching title patterns found.") + + 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"\U0001f50d 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"\U0001f50d Preserving existing labels: {', '.join(preserved_labels)}") + return preserved_labels + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description="Label pull requests based on changed file paths and PR title patterns." + ) + + parser.add_argument( + "pr_number", + 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", + action="store_true", + help="Add labels without removing existing ones (default)", + ) + mode_group.add_argument( + "-r", + "--replace", + action="store_true", + help="Replace all existing labels", + ) + + parser.add_argument( + "-d", + "--dry-run", + action="store_true", + help="Run without actually applying labels", + ) + + parser.add_argument( + "-c", + "--config", + default=DEFAULT_CONFIG_PATH, + help=f"Path to JSON config file (default: {DEFAULT_CONFIG_PATH})", + ) + + args, unknown = parser.parse_known_args() # required to handle --dry-run passed as an empty string ("") by the workflow + return args + +def main(): + args = parse_args() + config = load_config_json(args.config) + LABEL_TITLE_PATTERNS = config["title_patterns"] + LABEL_PATH_PATTERNS = config["path_patterns"] + + pr_number = args.pr_number + mode = "replace" if args.replace else "add" + + if args.dry_run: + print("\U0001f50d DRY RUN MODE \u2013 Labels will not be applied") + print(f"\U0001f680 Labeling mode: {mode}") + print(f"\U0001f50d Checking PR #{pr_number}...") + + pr_title = gh_get_pr_title(pr_number) + print(f"\U0001f4cb PR Title: {pr_title}\n") + + changed_files = gh_get_changed_files(pr_number) + print("\U0001f436 Changed files:\n" + "\n".join(changed_files) + "\n") + + filepath_labels = label_filepaths(changed_files, LABEL_PATH_PATTERNS) + title_labels = label_title(pr_title, LABEL_TITLE_PATTERNS) + all_labels = set(filepath_labels + title_labels) + + if all_labels: + print("----------------------------------------") + labels_str = ", ".join(sorted(all_labels)) + if mode == "add": + print(f"::notice::\U0001f4cb 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::\U0001f4cb Replacing labels with: {labels_str}") + if not args.dry_run: + gh_replace_labels(pr_number, list(all_labels)) + else: + print("::warning::No matching patterns found, no labels applied.") + + print("\u2705 Done") + +if __name__ == "__main__": + main() From 0d2ba3129893a5297f61742252afbe25ed575a8e Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Mon, 22 Jun 2026 22:24:54 +0200 Subject: [PATCH 2/5] Validate input of config file path --- auto-label-pr-sdlc/label-pr.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/auto-label-pr-sdlc/label-pr.py b/auto-label-pr-sdlc/label-pr.py index 063113e63..9240496ff 100644 --- a/auto-label-pr-sdlc/label-pr.py +++ b/auto-label-pr-sdlc/label-pr.py @@ -33,6 +33,12 @@ def load_config_json(config_file: str) -> dict: """Load configuration from JSON file.""" + resolved = os.path.realpath(config_file) + cwd = os.path.realpath(os.getcwd()) + if not resolved.startswith(cwd + os.sep): + print(f"\u274c Config file path escapes working directory: {config_file}") + sys.exit(1) + if not os.path.exists(config_file): print(f"\u274c Config file not found: {config_file}") sys.exit(1) From 63fee98cc02491426657de08e6eaafa5c20187b8 Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Mon, 22 Jun 2026 22:25:08 +0200 Subject: [PATCH 3/5] Validate provided PR number --- auto-label-pr-sdlc/label-pr.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/auto-label-pr-sdlc/label-pr.py b/auto-label-pr-sdlc/label-pr.py index 9240496ff..fb9f17451 100644 --- a/auto-label-pr-sdlc/label-pr.py +++ b/auto-label-pr-sdlc/label-pr.py @@ -25,6 +25,7 @@ import argparse import json import os +import re import subprocess import sys @@ -246,6 +247,13 @@ def parse_args(): args, unknown = parser.parse_known_args() # required to handle --dry-run passed as an empty string ("") by the workflow return args +def validate_pr_number(pr_number: str) -> None: + """Validate that pr_number is a positive integer to prevent argument/URL injection.""" + if not re.fullmatch(r'[1-9][0-9]*', pr_number): + print(f"\u274c Invalid PR number: {pr_number!r} (must be a positive integer)") + sys.exit(1) + + def main(): args = parse_args() config = load_config_json(args.config) @@ -253,6 +261,7 @@ def main(): LABEL_PATH_PATTERNS = config["path_patterns"] pr_number = args.pr_number + validate_pr_number(pr_number) mode = "replace" if args.replace else "add" if args.dry_run: From 2d295cadd522ffd49159f3d13704557f6d4007c3 Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Mon, 22 Jun 2026 22:30:24 +0200 Subject: [PATCH 4/5] Sanitize PR titles before logging --- auto-label-pr-sdlc/label-pr.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/auto-label-pr-sdlc/label-pr.py b/auto-label-pr-sdlc/label-pr.py index fb9f17451..5ca2fa7e1 100644 --- a/auto-label-pr-sdlc/label-pr.py +++ b/auto-label-pr-sdlc/label-pr.py @@ -270,7 +270,10 @@ def main(): print(f"\U0001f50d Checking PR #{pr_number}...") pr_title = gh_get_pr_title(pr_number) - print(f"\U0001f4cb PR Title: {pr_title}\n") + # Sanitize before logging: strip CR/LF to prevent log injection and replace "::" + # to prevent GitHub Actions from interpreting workflow commands in the title. + safe_title = pr_title.replace("\r", "").replace("\n", "").replace("::", ": :") + print(f"\U0001f4cb PR Title: {safe_title}\n") changed_files = gh_get_changed_files(pr_number) print("\U0001f436 Changed files:\n" + "\n".join(changed_files) + "\n") From d5a960912307f845ed47030144901cf326fe4012 Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Mon, 22 Jun 2026 22:42:21 +0200 Subject: [PATCH 5/5] Add tests for auto-label-pr-sdlc --- .github/workflows/test-auto-label-pr-sdlc.yml | 144 ++++++++++++++++++ .../tests/fixtures/label-pr.json | 9 ++ 2 files changed, 153 insertions(+) create mode 100644 .github/workflows/test-auto-label-pr-sdlc.yml create mode 100644 auto-label-pr-sdlc/tests/fixtures/label-pr.json diff --git a/.github/workflows/test-auto-label-pr-sdlc.yml b/.github/workflows/test-auto-label-pr-sdlc.yml new file mode 100644 index 000000000..9ee1cb26a --- /dev/null +++ b/.github/workflows/test-auto-label-pr-sdlc.yml @@ -0,0 +1,144 @@ +name: Test SDLC / Auto Label PR action + +on: + workflow_dispatch: + inputs: + pr_number: + description: "PR number to test against (required for matching tests)" + required: false + pull_request: + paths: + - "auto-label-pr-sdlc/**" + - ".github/workflows/test-auto-label-pr-sdlc.yml" + +permissions: {} + +jobs: + test-add-mode: + name: Test add mode (dry-run, should succeed) + runs-on: ubuntu-24.04 + permissions: + contents: read + pull-requests: read + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run auto-label-pr-sdlc (add mode, dry-run) + uses: ./auto-label-pr-sdlc + with: + pr-number: ${{ github.event.pull_request.number || inputs.pr_number }} + pr-labels: "[]" + mode: add + dry-run: "true" + config-path: auto-label-pr-sdlc/tests/fixtures/label-pr.json + github-token: ${{ secrets.GITHUB_TOKEN }} + + test-replace-mode: + name: Test replace mode preserves non-type labels (dry-run, should succeed) + runs-on: ubuntu-24.04 + permissions: + contents: read + pull-requests: read + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run auto-label-pr-sdlc (replace mode, dry-run) + uses: ./auto-label-pr-sdlc + with: + pr-number: ${{ github.event.pull_request.number || inputs.pr_number }} + pr-labels: '[{"name":"hold"},{"name":"t:bug"}]' + mode: replace + dry-run: "true" + config-path: auto-label-pr-sdlc/tests/fixtures/label-pr.json + github-token: ${{ secrets.GITHUB_TOKEN }} + + test-invalid-pr-number: + name: Test invalid PR number (should fail) + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run auto-label-pr-sdlc (invalid PR number) + uses: ./auto-label-pr-sdlc + with: + pr-number: not-a-number + pr-labels: "[]" + dry-run: "true" + config-path: auto-label-pr-sdlc/tests/fixtures/label-pr.json + github-token: ${{ secrets.GITHUB_TOKEN }} + + test-path-traversal: + name: Test config path traversal blocked (should fail) + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run auto-label-pr-sdlc (path traversal config) + uses: ./auto-label-pr-sdlc + with: + pr-number: "1" + pr-labels: "[]" + dry-run: "true" + config-path: ../../../etc/passwd + github-token: ${{ secrets.GITHUB_TOKEN }} + + validate: + name: Validate Test Results + runs-on: ubuntu-24.04 + needs: + - test-add-mode + - test-replace-mode + - test-invalid-pr-number + - test-path-traversal + if: always() + permissions: + contents: read + env: + _ADD_MODE: ${{ needs.test-add-mode.result }} + _REPLACE_MODE: ${{ needs.test-replace-mode.result }} + _INVALID_PR_NUMBER: ${{ needs.test-invalid-pr-number.result }} + _PATH_TRAVERSAL: ${{ needs.test-path-traversal.result }} + steps: + - name: Validate results + run: | + failed=false + + check_result() { + local name="$1" + local result="$2" + local expected="$3" + if [[ "$result" == "$expected" ]]; then + echo "✅ [$name] correctly got [$result]" | tee -a $GITHUB_STEP_SUMMARY + else + echo "❌ [$name] expected [$expected] but got [$result]" | tee -a $GITHUB_STEP_SUMMARY + failed=true + fi + } + + check_result "test-add-mode" "$_ADD_MODE" "success" + check_result "test-replace-mode" "$_REPLACE_MODE" "success" + check_result "test-invalid-pr-number" "$_INVALID_PR_NUMBER" "failure" + check_result "test-path-traversal" "$_PATH_TRAVERSAL" "failure" + + if [[ "$failed" == "true" ]]; then + echo "❌ One or more test results were unexpected. See above for details." | tee -a $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "✅ All test results are as expected." | tee -a $GITHUB_STEP_SUMMARY diff --git a/auto-label-pr-sdlc/tests/fixtures/label-pr.json b/auto-label-pr-sdlc/tests/fixtures/label-pr.json new file mode 100644 index 000000000..32cb9c1b9 --- /dev/null +++ b/auto-label-pr-sdlc/tests/fixtures/label-pr.json @@ -0,0 +1,9 @@ +{ + "title_patterns": { + "t:feature": ["feat"], + "t:bug": ["fix"] + }, + "path_patterns": { + "t:feature": ["auto-label-pr-sdlc/"] + } +}