Fix: Resolve open cache-isolation and config-provenance issues - #393
Open
ModeSevenIndustrialSolutions wants to merge 4 commits into
Open
Fix: Resolve open cache-isolation and config-provenance issues#393ModeSevenIndustrialSolutions wants to merge 4 commits into
ModeSevenIndustrialSolutions wants to merge 4 commits into
Conversation
basedpyright reported 104 errors across tests/ that src/ was already free of, because both the pre-commit hook and the [tool.pyright] include list covered src/ alone. Most were genuine. Tests passed a str where Inputs.gerrit_server_port is declared int, passed "" where a Path | None was expected, and set Orchestrator.workspace to a str immediately after the constructor had already stored the Path. Thirteen loops searched action.yaml for a named step and then used the result without checking that the search had found anything, so a renamed step surfaced as an opaque AttributeError on None rather than a message naming what was missing. Test stand-ins for GerritInfo and GitHubContext now build the real dataclasses, which checks their field names too. Inputs keeps its SimpleNamespace stand-in behind a cast, because reconstructing ~30 required fields to set four reconciliation switches would bury what each test actually varies. Also drops a click compatibility shim from _get_combined_output. It guarded against click below 8.2, where CliRunner mixes stderr into stdout and result.stderr raises ValueError. Three other CliRunner call sites already read result.stderr unguarded, so the suite has required click 8.2 for some time; one helper compensating on its own bought nothing. No assertion was removed or weakened, and no suppression comment was added. The suite collects and passes the same 1518 tests as before. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Matthew Watkins <mwatkins@linuxfoundation.org>
basedpyright and ruff covered src/ alone, so tests/, scripts/ and sitecustomize.py accumulated errors that no gate would catch. An editor language server reads [tool.pyright] directly and checks whatever file is open, so those errors surfaced during development while CI stayed green. The include list and the hook's files pattern now name every Python path in the repository, and each cross-references the other so the two stay together. Tests keep the annotation latitude they already had under mypy, through an executionEnvironment that disables reportMissingParameterType. Annotating several hundred monkeypatch and tmp_path parameters would add no checking beyond what pytest's name-based fixture resolution performs at collection time. Correctness rules still apply to tests in full. reportUntypedFunctionDecorator is off for tests as well, because it fires on every @pytest.fixture whenever pytest is not importable, and the hook runs basedpyright without a project virtualenv. The repository already tolerates that condition through reportMissingImports. The dev extra now pins click>=8.2. Several tests read CliRunner's result.stderr, which click captures separately only from 8.2 onward and otherwise raises ValueError, so this records a requirement the suite already had. The runtime floor stays at 8.1.7, which remains accurate for the package itself. Finally, the dev extra is referenced from the default dependency group. uv installs that group for a bare `uv sync`, so `uv run pytest` picks up the project's pinned pytest; previously the extra went uninstalled and the call fell through to whichever pytest sat on PATH. The extra stays an extra because CI installs it by name and cannot see dependency groups. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Matthew Watkins <mwatkins@linuxfoundation.org>
Completes issue lfreleng-actions#385. An autouse fixture already cleared gerrit_urls._BASE_PATH_CACHE; the sibling caches it named were left to a follow-up. ssh_config_parser memoises four values, and each derives from state tests routinely monkeypatch. Two are lru_cache(maxsize=1) over zero-argument functions reading G2G_RESPECT_USER_SSH and git config user.email, so with one slot and no key *any* two tests collide and the second sees the first one's environment. derive_gerrit_credentials keys on (host, organization, port) while its answer also depends on those two caches and on ~/.ssh/config, none of which appear in the key. _ssh_config_cache keys on the config path, never its contents, so rewriting the file under an unchanged HOME serves the previous parse. The module already exposed clear_credential_cache(), which resets all four, but only two test modules called it and only from setup_method: that guards those files on the way in while leaving the caches populated for whatever runs next. Calling it from an autouse fixture covers every test, and clearing on both sides stops a dirty cache reaching a test as well as leaving one behind. No src/ change was needed. No leak between existing tests was demonstrable, so this is latent rather than live. Every test reaching derive_gerrit_credentials either patches it wholesale or clears in setup_method, which is why it has held so far. It would bite whenever someone adds a test that derives credentials for real, and would surface far from its cause. The new tests pair a polluter with a victim for each cache. Verified non-vacuous: with the autouse fixtures disabled all five victims fail, with stale environment, stale git identity and a stale SSH config parse respectively. The explicit clear in test_gerrit_urls_more.py at module scope is now redundant and goes. The one mid-test survives, because it is not a boundary: without it the second half of that test never probes at all and its assertions pass off the value cached by the first half. Its comment claimed the fixture covered it, which was wrong. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Matthew Watkins <mwatkins@linuxfoundation.org>
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
August 26, 2026 08:22
View session
There was a problem hiding this comment.
Pull request overview
Resets process-wide caches between tests and tracks derived Gerrit configuration so duplicate detection can prefer .gitreview. The diff also contains stacked #392 static-analysis changes.
Changes:
- Adds cache/provenance isolation fixtures and regression tests.
- Tracks derived configuration and adjusts duplicate-detection precedence.
- Updates stacked test typing and static-analysis configuration.
Reviewed changes
Copilot reviewed 28 out of 29 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/github2gerrit/config.py |
Adds derived-key provenance tracking. |
src/github2gerrit/duplicate_detection.py |
Prefers .gitreview over derived values. |
src/github2gerrit/cli.py |
Controls provenance during config persistence. |
tests/conftest.py |
Resets global caches and provenance. |
tests/test_cache_isolation.py |
Tests cache isolation regressions. |
tests/test_config_helpers.py |
Tests provenance behavior. |
tests/test_duplicate_detection.py |
Tests duplicate resolution precedence. |
tests/test_gerrit_urls_more.py |
Removes redundant cache fixture. |
tests/test_ssh_artifact_prevention.py |
Adds type-narrowing assertions. |
tests/test_reconciliation_plan_and_orphans.py |
Uses typed test models. |
tests/test_reconciliation_extracted_module.py |
Uses typed test models. |
tests/test_pr_content_filter_integration.py |
Corrects port type. |
tests/test_pr_commands.py |
Adds type annotations. |
tests/test_orphan_rest_side_effects.py |
Uses typed Gerrit information. |
tests/test_mapping_comment_digest_and_backref.py |
Corrects model argument types. |
tests/test_mapping_comment_additional.py |
Uses typed test models. |
tests/test_gerrit_pr_closer.py |
Narrows parsed result type. |
tests/test_gerrit_change_id_footer.py |
Corrects port type. |
tests/test_core_ssh_setup.py |
Corrects port types. |
tests/test_core_prepare_commits.py |
Corrects port type. |
tests/test_core_integration_fixture_repo.py |
Corrects port type. |
tests/test_composite_action_coverage.py |
Adds missing-step assertions. |
tests/test_cli.py |
Targets Click 8.2 output behavior. |
tests/test_action_outputs.py |
Adds missing-step assertion. |
tests/test_action_environment_mapping.py |
Adds missing-step assertions. |
tests/fixtures/make_repo.py |
Simplifies byte-output decoding. |
pyproject.toml |
Extends static analysis and dev dependencies. |
.pre-commit-config.yaml |
Broadens Python hook coverage. |
uv.lock |
Locks updated development dependencies. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
August 26, 2026 08:34
View session
ModeSevenIndustrialSolutions
force-pushed
the
fix/open-issues
branch
from
August 26, 2026 08:52
726484a to
a7cd3f0
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
August 26, 2026 08:53
View session
ModeSevenIndustrialSolutions
force-pushed
the
fix/open-issues
branch
from
August 26, 2026 09:12
a7cd3f0 to
54ab1eb
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
August 26, 2026 09:13
View session
ModeSevenIndustrialSolutions
force-pushed
the
fix/open-issues
branch
from
August 26, 2026 09:24
54ab1eb to
9d3c0c3
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
August 26, 2026 09:25
View session
Closes issue lfreleng-actions#386. Duplicate detection short-circuited on GERRIT_SERVER and GERRIT_PROJECT from the environment, but by the time it runs those are usually derived fallbacks rather than operator intent: _load_effective_inputs derives them and writes them to the environment first. Two consequences. The .gitreview resolution below that early return was effectively dead on the normal path, including the base-ref pinning added for fork pull requests. And the derived project is a guess taken from the GitHub repository name, so the OpenDaylight repository integration-distribution yielded that same string where Gerrit holds integration/distribution. Detection queried a project that does not exist and reported no duplicates, silently. Config values now carry provenance. mark_derived_keys records which keys hold a derived fallback and is_derived_key reports it, backed by G2G_DERIVED_KEYS. That is process-scoped configuration, settled once before the bulk thread pool starts, so unlike per-pull-request state it is safe to hold in the environment. apply_parameter_derivation marks only those keys whose environment value is empty, matching the test apply_config_to_env applies moments later; a key derivation supplies but the environment already defines explicitly stays explicit. The save-after-success call site opts out, since it persists to the config file without ever touching the environment. Duplicate detection now short-circuits only when both host and project are explicit, letting the per-pull-request .gitreview resolve otherwise. Where .gitreview yields nothing the derived pair still applies, because returning nothing would disable detection entirely wherever it previously ran against a derived project; a query against a possibly wrong project is no worse than no query. The fork-trust handling is untouched. Provenance is tracked per field, so a partially configured target keeps operator intent: where only one of the pair is derived, .gitreview supplies that field alone and the explicit one survives, and only where it carries a value. A .gitreview naming a host but no project parses successfully, and taking its empty project would drop the project qualifier from the query and search every project on the server, letting an unrelated change of the same title block a legitimate submission. A GERRIT_PROJECT arriving from the configuration file counts as derived, whichever release or hand wrote it. That file is sectioned per-organization while a project is per-repository, so a value there is wrong for every repository in the org but one; treating it as a fallback keeps it usable while letting .gitreview outrank it, and covers configurations written before such saves stopped. Derived parameters persisted to the configuration file now omit GERRIT_PROJECT. That value is per-repository while the file section is per-organization, so saving it handed every other repository in the org one repository's project name, and it returned on the next run as configuration indistinguishable from operator intent. Verified against the unmodified detector: the new test for a project containing a path separator fails there, showing the hyphenated name in the live query URL. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Matthew Watkins <mwatkins@linuxfoundation.org>
ModeSevenIndustrialSolutions
force-pushed
the
fix/open-issues
branch
from
August 26, 2026 09:47
9d3c0c3 to
972613c
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
August 26, 2026 09:47
View session
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 29 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/github2gerrit/config.py:828
- This provenance block is skipped when
G2G_ENABLE_DERIVATION=false, becauseapply_parameter_derivationreturns at lines 770–775 first. A legacy org-levelGERRIT_PROJECTis then exported without a derived mark, so duplicate detection treats it as explicit and again short-circuits before.gitreview, reproducing #386 for a supported configuration. Record provenance for config-sourced projects independently of whether new derivation is enabled, and add the disabled-derivation regression case.
if (
mark_derived
and cfg.get("GERRIT_PROJECT", "").strip()
and (os.getenv("GERRIT_PROJECT") or "").strip() == ""
):
mark_derived_keys(["GERRIT_PROJECT"])
ModeSevenIndustrialSolutions
marked this pull request as ready for review
August 27, 2026 12:16
This was referenced Aug 27, 2026
askb
previously approved these changes
Aug 29, 2026
ModeSevenIndustrialSolutions
dismissed
askb’s stale review
August 29, 2026 10:24
The merge-base changed after approval.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #385
Closes #386
Note
#392 has merged, so this no longer stacks — the diff is now just the two commits below. Still marked draft; say the word and I'll mark it ready.
Fix: Reset process-wide caches between tests— #385The autouse fixture clearing
gerrit_urls._BASE_PATH_CACHElanded in #384; the sibling caches that issue named were left to a follow-up. This is that follow-up.ssh_config_parsermemoises four values, each derived from state tests routinely monkeypatch:_get_respect_user_ssh_settinglru_cache(maxsize=1)G2G_RESPECT_USER_SSH_get_cached_git_user_emaillru_cache(maxsize=1)git config user.emailderive_gerrit_credentials(host, organization, port)~/.ssh/configand env — none of which are in the key_ssh_config_cacheThe first two are the worst: with no arguments there is a single cache slot, so any two tests collide and the second receives the first one's environment — including the git identity that
isolate_git_environmentworks to control.The module already exposed
clear_credential_cache(), which resets all four, but only two test modules called it and only fromsetup_method— guarding those files on the way in while leaving the caches populated for whatever runs next. Calling it from an autouse fixture covers every test, and clearing on both sides stops a dirty cache reaching a test as well as leaving one behind. Nosrc/change was needed.Honest scoping: no leak between existing tests is demonstrable. Every test reaching
derive_gerrit_credentialseither patches it wholesale or clears insetup_method, which is why this has held. The exposure is latent, exactly like the base-path bug — it bites when someone adds a test that derives credentials for real, and surfaces far from its cause. No leak is claimed that could not be shown.The explicit clear in
test_gerrit_urls_more.pyat module scope is now redundant and goes. The one mid-test survives, because it is not a test boundary: without it the second half of that test never probes at all and its assertions pass off the value cached by the first half. Its comment claimed the fixture covered it, which was wrong; the comment is now accurate.Fix: Separate derived Gerrit config from explicit— #386Duplicate detection short-circuited on
GERRIT_SERVER/GERRIT_PROJECTfrom the environment, but by then those are usually derived fallbacks, not operator intent —_load_effective_inputsderives them and writes them to the environment first.Two consequences: the
.gitreviewresolution below that early return was effectively dead on the normal path, including the base-ref pinning added in #384 for fork PRs; and the derived project is a guess taken from the repository name, so OpenDaylight'sintegration-distributionyielded that same string where Gerrit holdsintegration/distribution. Detection queried a project that does not exist and reported no duplicates, silently.Config values now carry provenance.
mark_derived_keysrecords which keys hold a derived fallback andis_derived_keyreports it, backed byG2G_DERIVED_KEYS.On why an environment variable is acceptable here: this is process-scoped configuration, settled once before the bulk
ThreadPoolExecutorstarts — deliberately unlike the per-PR state thatcli.py:289already documents as unsafe to hold globally.Marking applies only to keys whose environment value is empty, matching the test
apply_config_to_envapplies moments later, so a key derivation supplies but the environment already defines explicitly stays explicit. Thecli.py:767call site opts out viamark_derived=False: it persists to the org config file after successful submission and never touches the environment.Detection now short-circuits only when both host and project are explicit, field by field, letting the per-PR
.gitreviewfill only what is not explicitly configured — and only where.gitreviewactually carries a value. Where it yields nothing the derived pair still applies, because returningNonewould disable detection entirely wherever it previously ran against a derived project. The fork-trust handling from #384 is untouched.Review round
Copilot found five issues across this PR, all genuine, all fixed. Three were bugs I introduced; two were pre-existing problems the provenance work exposed. Worth recording, since two of them were worse than the bug this PR set out to fix:
derivedflag meant one derived key made the whole pair non-explicit, so.gitreviewreplaced an explicitly configured value too. Now tracked per field..gitreview.parse_gitreviewrequires onlyhost=and returnsproject = "". Taking that empty project dropped theproject:qualifier from the query —_build_duplicate_query_pathlogs it asproject=(any)— so detection searched every project on the server, letting an unrelated same-title change block a legitimate submission. Now falls back to the environment value per field.save_derived_parameters_to_configwrites into the org section, so one repository'sGERRIT_PROJECTwas handed to every sibling repository. Now filtered out before writing.GERRIT_PROJECTfrom the config file is now treated as derived outright — the file is sectioned per-organization while a project is per-repository, so such a value is misscoped whoever wrote it. It stays usable as a last resort, so nothing breaks and no user file is rewritten."raw.githubusercontent.com" in urlaccepted lookalike hosts, path segments, query strings and plain HTTP. Now parsed and compared as an origin.One deliberate behaviour change from (2): when
.gitreviewis host-only and no project is configured anywhere, the resolver returnsNoneand the check is skipped, where it previously issued the unscoped query. A missed duplicate is recoverable; a false positive blocking a legitimate submission is not.Validation
pytestbasedpyright(124 files)ruff check/formataislop ciprek run --all-filesEvery fix was verified by deleting it and watching the tests fail, not by observing them pass. Representative:
Two of the provenance tests pass with and without their fix — with the block absent nothing marks the key, so "not marked" is trivially true. They guard against a plausible mis-fix rather than proving the fix, and are flagged as such rather than counted as regression tests.
A note on the aislop gate
The first cut of #386 added 47 lines to
duplicate_detection.pyand trippedcomplexity/file-too-large. Rather than add an ignore directive, I measured the real budget — the gate warns at 991 lines, a 10% grace band over the configuredmax: 900, and the file sat at 978 — then expressed the same logic, twice over as review findings landed, by moving rationale into the docstring and condensing comments. Final: 989 lines, gate at 100/100. No threshold lowered, no exclusion added.Deliberately not done
core._resolve_gerrit_infostill returns.gitreviewunconditionally, ahead of an explicitly configuredGERRIT_SERVER— the "arguably backwards" precedence Duplicate detection cannot distinguish derived config #386 calls out.is_derived_keyis now the tool to fix it, but it touches the push path and deserves its own change.config._read_gitreview_hoststill reads the local working-directory file — the residual exposure from Duplicate detection cannot distinguish derived config #386's comment thread, needing PR provenance at derivation time.derive_gerrit_credentials' memo is unsound in production, not merely in tests. Its real inputs — an env var, git config,~/.ssh/config— are absent from its key, andcli.py:2329mutatesG2G_RESPECT_USER_SSHat runtime, so whether that mutation is visible depends on call ordering.pull_request_reviewprivileges or skip to the two-stageworkflow_runredesign.