From a2af96a200bddcd25cc9f7e429e595ae00fe6103 Mon Sep 17 00:00:00 2001 From: zhewenli Date: Wed, 10 Sep 2025 15:27:40 -0700 Subject: [PATCH 1/3] support custom dir --- .github/actions/bc-lint/action.yml | 7 +++- tools/stronghold/docs/bc_linter_config.md | 6 ++-- tools/stronghold/src/api/checker.py | 10 +++++- tools/stronghold/src/api/config.py | 32 +++++++++++++++---- .../tests/api/test_config_loader.py | 20 ++++++++++++ 5 files changed, 64 insertions(+), 11 deletions(-) diff --git a/.github/actions/bc-lint/action.yml b/.github/actions/bc-lint/action.yml index 02d306e90d..b2a84d3188 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 != '' && '--config-dir=' + 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..c4d6ad5384 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,9 @@ def run() -> None: print("::endgroup::") # Load config and optionally print when detected - cfg, cfg_status = api.config.load_config_with_status(repo.dir) + cfg, cfg_status = api.config.load_config_with_status( + repo.dir, config_dir=args.config_dir + ) 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..cca92bd0e2 100644 --- a/tools/stronghold/src/api/config.py +++ b/tools/stronghold/src/api/config.py @@ -54,15 +54,29 @@ 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( + repo_root: pathlib.Path, *, config_dir: pathlib.Path | str | None = None +) -> tuple[Config, str]: + """Loads configuration from `.bc-linter.yml`. + + 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``. 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_base = repo_root + if config_dir is not None: + cfg_dir_path = pathlib.Path(config_dir) + if not cfg_dir_path.is_absolute(): + cfg_dir_path = repo_root / cfg_dir_path + cfg_base = cfg_dir_path + + cfg_path = cfg_base / ".bc-linter.yml" if not cfg_path.exists(): return (default_config(), "default_missing") @@ -159,12 +173,16 @@ 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( + repo_root: pathlib.Path, *, config_dir: pathlib.Path | str | None = None +) -> Config: + """Loads configuration from `.bc-linter.yml`. - If the file does not exist or cannot be parsed, returns defaults. + By default the configuration file is read from the repository root; a + custom directory may be supplied via ``config_dir``. If the file does not + exist or cannot be parsed, defaults are returned. """ - cfg, _ = load_config_with_status(repo_root) + cfg, _ = load_config_with_status(repo_root, config_dir=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..205f2c5049 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: + sub = tmp_path / "subdir" + sub.mkdir() + yml = textwrap.dedent( + """ + version: 1 + include: ["src/**/*.py"] + """ + ) + (sub / ".bc-linter.yml").write_text(yml) + + cfg_rel, status_rel = load_config_with_status(tmp_path, config_dir="subdir") + assert status_rel == "parsed" + assert cfg_rel.include == ["src/**/*.py"] + + cfg_abs, status_abs = load_config_with_status(tmp_path, config_dir=sub) + assert status_abs == "parsed" + assert cfg_abs.include == ["src/**/*.py"] From 17f8e4e416799ce7f01326be7bca8d6a559aeb4e Mon Sep 17 00:00:00 2001 From: zhewenli Date: Wed, 10 Sep 2025 16:15:27 -0700 Subject: [PATCH 2/3] move parsing to checker --- tools/stronghold/src/api/checker.py | 10 ++++-- tools/stronghold/src/api/config.py | 32 ++++--------------- .../tests/api/test_config_loader.py | 10 +++--- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/tools/stronghold/src/api/checker.py b/tools/stronghold/src/api/checker.py index c4d6ad5384..85b0560797 100644 --- a/tools/stronghold/src/api/checker.py +++ b/tools/stronghold/src/api/checker.py @@ -52,9 +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, config_dir=args.config_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 cca92bd0e2..ed3df8f87d 100644 --- a/tools/stronghold/src/api/config.py +++ b/tools/stronghold/src/api/config.py @@ -54,14 +54,8 @@ def default_config() -> Config: return Config() -def load_config_with_status( - repo_root: pathlib.Path, *, config_dir: pathlib.Path | str | None = None -) -> tuple[Config, str]: - """Loads configuration from `.bc-linter.yml`. - - 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``. +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 @@ -69,14 +63,7 @@ def load_config_with_status( - 'default_error' -> file existed but YAML missing/invalid or parser unavailable """ - cfg_base = repo_root - if config_dir is not None: - cfg_dir_path = pathlib.Path(config_dir) - if not cfg_dir_path.is_absolute(): - cfg_dir_path = repo_root / cfg_dir_path - cfg_base = cfg_dir_path - - cfg_path = cfg_base / ".bc-linter.yml" + cfg_path = config_dir / ".bc-linter.yml" if not cfg_path.exists(): return (default_config(), "default_missing") @@ -173,16 +160,11 @@ def _ann_list(raw: Any) -> list[AnnotationSpec]: return (cfg, "parsed") -def load_config( - repo_root: pathlib.Path, *, config_dir: pathlib.Path | str | None = None -) -> Config: - """Loads configuration from `.bc-linter.yml`. - - By default the configuration file is read from the repository root; a - custom directory may be supplied via ``config_dir``. If the file does not - exist or cannot be parsed, defaults are returned. +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, config_dir=config_dir) + 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 205f2c5049..2c319f0eed 100644 --- a/tools/stronghold/tests/api/test_config_loader.py +++ b/tools/stronghold/tests/api/test_config_loader.py @@ -152,20 +152,20 @@ def test_warn_on_unknown_top_level_keys(tmp_path: pathlib.Path, capsys) -> None: def test_load_config_from_custom_directory(tmp_path: pathlib.Path) -> None: - sub = tmp_path / "subdir" - sub.mkdir() + config_dir = tmp_path / "subdir" + config_dir.mkdir() yml = textwrap.dedent( """ version: 1 include: ["src/**/*.py"] """ ) - (sub / ".bc-linter.yml").write_text(yml) + (config_dir / ".bc-linter.yml").write_text(yml) - cfg_rel, status_rel = load_config_with_status(tmp_path, config_dir="subdir") + 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(tmp_path, config_dir=sub) + cfg_abs, status_abs = load_config_with_status(config_dir) assert status_abs == "parsed" assert cfg_abs.include == ["src/**/*.py"] From dc7c68ea4372d7d302554c42fe60e4fb0a659e93 Mon Sep 17 00:00:00 2001 From: zhewenli Date: Wed, 10 Sep 2025 16:44:57 -0700 Subject: [PATCH 3/3] update yml --- .github/actions/bc-lint/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/bc-lint/action.yml b/.github/actions/bc-lint/action.yml index b2a84d3188..0615ecec43 100644 --- a/.github/actions/bc-lint/action.yml +++ b/.github/actions/bc-lint/action.yml @@ -71,7 +71,7 @@ runs: --base-commit=${{ inputs.base_sha }} \ --head-commit=${{ steps.merge_changes.outputs.new_head_sha }} \ ${{ inputs.suppression == 'true' && '--suppressed' || '' }} \ - ${{ inputs.config_dir != '' && '--config-dir=' + inputs.config_dir || '' }} + ${{ inputs.config_dir != '' && format('--config-dir={0}', inputs.config_dir) || '' }} - name: Display documentation link if failed if: ${{ failure() && inputs.docs_link }}