diff --git a/.github/actions/bc-lint/action.yml b/.github/actions/bc-lint/action.yml index 02d306e90d..0615ecec43 100644 --- a/.github/actions/bc-lint/action.yml +++ b/.github/actions/bc-lint/action.yml @@ -19,6 +19,10 @@ inputs: description: 'Link to the docs to display in case of failure' required: false default: '' + config_dir: + description: 'Directory to load .bc-linter.yml from (defaults to repository root)' + required: false + default: '' runs: using: 'composite' steps: @@ -66,7 +70,8 @@ runs: ../_test-infra/tools/stronghold/bin/check-api-compatibility \ --base-commit=${{ inputs.base_sha }} \ --head-commit=${{ steps.merge_changes.outputs.new_head_sha }} \ - ${{ inputs.suppression == 'true' && '--suppressed' || '' }} + ${{ inputs.suppression == 'true' && '--suppressed' || '' }} \ + ${{ inputs.config_dir != '' && format('--config-dir={0}', inputs.config_dir) || '' }} - name: Display documentation link if failed if: ${{ failure() && inputs.docs_link }} diff --git a/tools/stronghold/docs/bc_linter_config.md b/tools/stronghold/docs/bc_linter_config.md index 89d71b5c2a..e16944811b 100644 --- a/tools/stronghold/docs/bc_linter_config.md +++ b/tools/stronghold/docs/bc_linter_config.md @@ -5,8 +5,10 @@ The config enables repo‑specific path selection, rule suppression, and custom annotations to include/exclude specific APIs. ### Config file location -- Place a YAML file named `.bc-linter.yml` at the repository root being linted - (the target repo). +- By default the linter searches for a `.bc-linter.yml` file at the root of + the repository being linted. +- Provide an alternative directory with `--config-dir` if the file lives + somewhere else. - If the file is missing or empty, defaults are applied (see below). ### Schema (YAML) diff --git a/tools/stronghold/src/api/checker.py b/tools/stronghold/src/api/checker.py index d54062a9b6..85b0560797 100644 --- a/tools/stronghold/src/api/checker.py +++ b/tools/stronghold/src/api/checker.py @@ -32,6 +32,12 @@ def run() -> None: action="store_true", help="Enable verbose output", ) + parser.add_argument( + "--config-dir", + type=str, + default=None, + help="Directory to load .bc-linter.yml from (defaults to repository root)", + ) args = parser.parse_args(sys.argv[1:]) repo = api.git.Repository(pathlib.Path(".")) @@ -46,7 +52,13 @@ def run() -> None: print("::endgroup::") # Load config and optionally print when detected - cfg, cfg_status = api.config.load_config_with_status(repo.dir) + # By default, configuration is loaded from the repository root. A custom + # configuration directory may be provided via ``config_dir`` either as an + # absolute path or a path relative to ``repo_root``. + cfg_path = pathlib.Path(args.config_dir) if args.config_dir else repo.dir + if not cfg_path.is_absolute(): + cfg_path = repo.dir / cfg_path + cfg, cfg_status = api.config.load_config_with_status(cfg_path) if cfg_status == "parsed": # Explicitly log successful config discovery and parsing print("BC-linter: Using .bc-linter.yml (parsed successfully)") diff --git a/tools/stronghold/src/api/config.py b/tools/stronghold/src/api/config.py index e7f34be722..ed3df8f87d 100644 --- a/tools/stronghold/src/api/config.py +++ b/tools/stronghold/src/api/config.py @@ -54,15 +54,16 @@ def default_config() -> Config: return Config() -def load_config_with_status(repo_root: pathlib.Path) -> tuple[Config, str]: - """Loads configuration from `.bc-linter.yml` in the given repository root. +def load_config_with_status(config_dir: pathlib.Path) -> tuple[Config, str]: + """Loads configuration from `.bc-linter.yml` in the given directory. Returns (config, status) where status is one of: - 'parsed' -> config file existed and parsed successfully - 'default_missing' -> no config file found - 'default_error' -> file existed but YAML missing/invalid or parser unavailable """ - cfg_path = repo_root / ".bc-linter.yml" + + cfg_path = config_dir / ".bc-linter.yml" if not cfg_path.exists(): return (default_config(), "default_missing") @@ -159,12 +160,11 @@ def _ann_list(raw: Any) -> list[AnnotationSpec]: return (cfg, "parsed") -def load_config(repo_root: pathlib.Path) -> Config: - """Loads configuration from `.bc-linter.yml` in the given repository root. - +def load_config(config_dir: pathlib.Path) -> Config: + """Loads configuration from `.bc-linter.yml` in the given directory. If the file does not exist or cannot be parsed, returns defaults. """ - cfg, _ = load_config_with_status(repo_root) + cfg, _ = load_config_with_status(config_dir) return cfg diff --git a/tools/stronghold/tests/api/test_config_loader.py b/tools/stronghold/tests/api/test_config_loader.py index c83af18842..2c319f0eed 100644 --- a/tools/stronghold/tests/api/test_config_loader.py +++ b/tools/stronghold/tests/api/test_config_loader.py @@ -149,3 +149,23 @@ def test_warn_on_unknown_top_level_keys(tmp_path: pathlib.Path, capsys) -> None: assert status == "parsed" out = capsys.readouterr().out assert "::warning::BC-linter: Unknown keys in .bc-linter.yml: ['typpo']" in out + + +def test_load_config_from_custom_directory(tmp_path: pathlib.Path) -> None: + config_dir = tmp_path / "subdir" + config_dir.mkdir() + yml = textwrap.dedent( + """ + version: 1 + include: ["src/**/*.py"] + """ + ) + (config_dir / ".bc-linter.yml").write_text(yml) + + cfg_rel, status_rel = load_config_with_status(config_dir) + assert status_rel == "parsed" + assert cfg_rel.include == ["src/**/*.py"] + + cfg_abs, status_abs = load_config_with_status(config_dir) + assert status_abs == "parsed" + assert cfg_abs.include == ["src/**/*.py"]