Skip to content

fix(pr_agent/servers/bitbucket_app.py): building the secret provider per process - #2736

Open
dwin-gharibi wants to merge 5 commits into
The-PR-Agent:mainfrom
dwin-gharibi:fix/bitbucket-fork-safe-secret-provider
Open

fix(pr_agent/servers/bitbucket_app.py): building the secret provider per process#2736
dwin-gharibi wants to merge 5 commits into
The-PR-Agent:mainfrom
dwin-gharibi:fix/bitbucket-fork-safe-secret-provider

Conversation

@dwin-gharibi

Copy link
Copy Markdown
Contributor

Closes #2735.

Description

bitbucket_app.py:30 constructs the secret provider at module import, and gunicorn_config.py:160 sets preload_app = True, so the client is created in the master and inherited by every forked worker.

Root cause

This is the exact hazard gitlab_webhook.py:36-48 already fixes and documents:

"Nothing is constructed at import because gunicorn runs with preload_app: a client built there
would belong to the master, and every worker would inherit its pooled connection. Keying the cache
on the pid means a forked worker always builds its own, and never adopts one created in another
process."

bitbucket_app.py had 0 references to a fork-safe accessor — the fix was applied to GitLab only.

The fix

Replace the module-level secret_provider = ... with get_fork_safe_secret_provider(), a
pid-keyed accessor mirroring gitlab_webhook.get_fork_safe_secret_provider(). Both call sites
(handle_github_webhooks, handle_installed_webhooks) go through it.

Behaviour change

Before One client built in the gunicorn master, inherited by every worker
After Each worker builds its own client on first use; a cache entry from another pid is never adopted

Files changed

pr_agent/servers/bitbucket_app.py | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

Testing

New regression coverage in tests/unittest/test_bitbucket_fork_safe_secret_provider.py4 tests, each written to fail
without the fix:

$ PYTHONPATH=. pytest tests/unittest/test_bitbucket_fork_safe_secret_provider.py
4 passed

Proven to be a genuine regression test: with every changed pr_agent/ file reverted to its
origin/main version and the new test file left in place, the suite fails. It only passes
with the fix applied.

Full pipeline, reproduced locally exactly as .github/workflows/build-and-test.yaml runs it:

docker build -f docker/Dockerfile --target test .
docker run --rm <image> pytest -v tests/unittest
-> 1965 passed, 1 skipped, 1 xfailed

on python:3.12.13-slim — no failures.

Also checked:

  • pytest tests/unittest — no new failures vs main
  • ruff — no new findings vs main; isort — clean on every file touched
  • No new code comments authored

Risk / compatibility

Deployments without CONFIG.SECRET_PROVIDER still get None, exactly as before. The accessor adds one dict lookup per webhook.

Checklist

  • Focused on a single fix
  • Existing tests pass
  • New regression tests added, proven to fail without the fix
  • No new dependencies
  • Reviewed by a maintainer

Copilot AI lite review requested due to automatic review settings August 21, 2026 12:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Make Bitbucket secret provider fork-safe under gunicorn preload_app

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Build the Bitbucket secret provider lazily per worker PID to avoid forked connection sharing.
• Route webhook secret read/write through a fork-safe accessor instead of a module-level singleton.
• Add regression tests covering import-time construction, PID mismatch rebuild, and disabled config.
Diagram

graph TD
  GM(["Gunicorn master"]) -->|"preload_app + fork"| GW(["Gunicorn worker"]) -->|"handles webhook"| BA["bitbucket_app"] -->|"needs secrets"| FS["fork-safe accessor"] --> ST[("PID-keyed state")]
  ST -->|"build/return"| SP["secret provider"] --> SB{{"secret backend"}}

  subgraph Legend
    direction LR
    _svc(["Runtime process"]) ~~~ _mod["Python module"] ~~~ _st[("Cache/state")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Disable gunicorn preload_app
  • ➕ Avoids import-time construction hazards globally
  • ➕ No extra PID cache logic in application code
  • ➖ Higher startup cost per worker (re-imports)
  • ➖ May break current performance/operational assumptions
  • ➖ Does not help other preloading/fork-related client reuse elsewhere
2. Use gunicorn post_fork hook to initialize provider
  • ➕ Keeps fork-handling centralized in deployment configuration
  • ➕ No PID checks on hot path
  • ➖ Ties correctness to gunicorn-specific wiring
  • ➖ Harder to test in unit tests
  • ➖ Less reusable if app runs under different process managers
3. Generalize into shared fork-safe cache utility
  • ➕ Avoids duplicating the same PID-keyed pattern across integrations
  • ➕ Standardizes behavior for other pooled clients
  • ➖ More refactor scope than needed for a focused bug fix
  • ➖ Requires agreement on common utility API and migration effort

Recommendation: Keep the PR’s current approach: a PID-keyed, lazy accessor local to bitbucket_app, mirroring the already-established gitlab_webhook pattern. It is minimal, testable, and removes the concrete fork-safety hazard introduced by preload_app without requiring deployment changes.

Files changed (2) +84 / -3

Bug fix (1) +14 / -3
bitbucket_app.pyAdd PID-keyed fork-safe secret provider accessor +14/-3

Add PID-keyed fork-safe secret provider accessor

• Replaces the module-import-time secret provider singleton with a lazy accessor keyed by os.getpid(). Updates webhook secret read and write call sites to always go through the fork-safe accessor so forked workers never reuse a provider built in another process.

pr_agent/servers/bitbucket_app.py

Tests (1) +70 / -0
test_bitbucket_fork_safe_secret_provider.pyAdd regression tests for per-process secret provider behavior +70/-0

Add regression tests for per-process secret provider behavior

• Introduces unit tests ensuring no provider is built at import time, that the provider is built on first use, that a cached provider from another PID is not adopted, and that missing configuration returns None.

tests/unittest/test_bitbucket_fork_safe_secret_provider.py

@qodo-code-review

qodo-code-review Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Lost startup config validation ✓ Resolved 🐞 Bug ☼ Reliability
Description
Removing the import-time get_secret_provider() call means a typo/invalid CONFIG.SECRET_PROVIDER (or
provider init failure) will no longer fail at startup, and will instead surface on first webhook
where the exception is caught and only logged, while the endpoint still returns "OK". This can leave
a seemingly healthy Bitbucket server that never processes webhooks (or intermittently fails) without
a hard startup signal.
Code

pr_agent/servers/bitbucket_app.py[30]

-secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None
+_secret_provider_state = {}
+
+
+def get_fork_safe_secret_provider():
+    """Return this process's secret provider, building it on first use after a fork."""
+    if not get_settings().get("CONFIG.SECRET_PROVIDER"):
+        return None
+    pid = os.getpid()
+    if _secret_provider_state.get("pid") != pid:
+        _secret_provider_state["provider"] = get_secret_provider()
+        _secret_provider_state["pid"] = pid
+    return _secret_provider_state["provider"]
Relevance

●●● Strong

Recent accepted GitLab precedent explicitly preserves startup validation while adding per-process
lazy rebuilding; this finding matches the intended pattern.

PR-#2550

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR removes Bitbucket’s import-time secret provider construction, which previously forced
provider validation/initialization at startup. Bitbucket now builds lazily inside
get_fork_safe_secret_provider() without any non-constructing validation, while the webhook handler
catches all exceptions and still returns "OK", masking configuration errors until runtime. GitLab’s
server shows the intended pattern: validate at import (without building) and build per PID on first
use.

pr_agent/servers/bitbucket_app.py[28-41]
pr_agent/servers/bitbucket_app.py[256-319]
pr_agent/secret_providers/init.py[7-17]
pr_agent/servers/gitlab_webhook.py[29-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`bitbucket_app.py` no longer constructs the secret provider at import (good for fork-safety), but that also removes the previous fail-fast behavior for invalid `CONFIG.SECRET_PROVIDER` values or provider initialization errors. Those errors will now happen at runtime on the first webhook, and in the `/webhook` handler they are swallowed by a broad `except`, while the endpoint returns "OK".

## Issue Context
A non-constructing validation helper already exists: `validate_secret_provider_setting()` in `pr_agent/secret_providers/__init__.py`, and GitLab’s webhook server calls it at import to preserve fail-fast behavior without building the client.

## Fix Focus Areas
- pr_agent/servers/bitbucket_app.py[19-41]
- pr_agent/secret_providers/__init__.py[7-17]
- pr_agent/servers/gitlab_webhook.py[29-33]

## Proposed change
1. Import `validate_secret_provider_setting` in `bitbucket_app.py`.
2. Call `validate_secret_provider_setting()` near module initialization (after settings are available, before request handling), mirroring `gitlab_webhook.py`.
3. Keep the lazy, PID-keyed `get_fork_safe_secret_provider()` to avoid pre-fork client creation.

This restores fast failure for invalid provider IDs while preserving the per-worker construction behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Brittle source-based assertion ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
test_the_setting_is_still_validated_at_import asserts on inspect.getsource(...) text rather than
verifying that validate_secret_provider_setting() actually runs during module import, so it can
pass even if runtime behavior regresses and can fail on harmless refactors (formatting/moving code).
This makes the regression coverage unreliable and increases test maintenance cost.
Code

tests/unittest/test_bitbucket_fork_safe_secret_provider.py[R75-80]

+def test_the_setting_is_still_validated_at_import():
+    """Keep failing at startup on a typo, which the removed import-time client used to catch."""
+    source = inspect.getsource(bitbucket_app)
+
+    assert "validate_secret_provider_setting()" in source
+
Relevance

●●● Strong

Recent accepted precedent explicitly replaces inspect.getsource assertions with behavior-based
tests, matching this finding closely.

PR-#2495

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The test currently asserts only that the string validate_secret_provider_setting() appears in the
module source, which is not equivalent to verifying the function is executed at import-time. The
production code change adds an actual import-time call, so the test should validate the behavioral
effect of that call (raising on unknown provider).

tests/unittest/test_bitbucket_fork_safe_secret_provider.py[75-80]
pr_agent/servers/bitbucket_app.py[33-34]
pr_agent/secret_providers/init.py[7-17]
PR-#2495

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`test_the_setting_is_still_validated_at_import()` currently checks for a substring in the module source via `inspect.getsource(bitbucket_app)`. This is brittle (fails on refactors/formatting) and can miss real regressions because it doesn't prove the call executes at import.

### Issue Context
The PR intentionally adds an import-time `validate_secret_provider_setting()` call in `pr_agent/servers/bitbucket_app.py`. The test should validate *observable behavior* (e.g., that an invalid `CONFIG.SECRET_PROVIDER` causes import/reload to raise), not source text.

### Fix Focus Areas
- tests/unittest/test_bitbucket_fork_safe_secret_provider.py[75-80]

### Implementation sketch
- Replace the `inspect.getsource(...)` assertion with a behavior test:
 - Monkeypatch `pr_agent.secret_providers.get_settings` (or `pr_agent.config_loader.get_settings`, depending on how you want to scope it) to return an invalid provider id.
 - Then `import importlib` and `import pr_agent.servers.bitbucket_app as bitbucket_app_module` and run `importlib.reload(bitbucket_app_module)` inside `pytest.raises(ValueError)`.
 - If needed, delete `sys.modules['pr_agent.servers.bitbucket_app']` before re-import to ensure import-time code runs.
- This directly verifies the import-time validation behavior without coupling to source layout.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Docstrings not imperative ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
New docstrings in the added unit test are written in descriptive form (e.g., gunicorn runs...,
Stands in...) instead of imperative phrasing. This violates the requirement for imperative
docstrings/comments and reduces consistency with project style guidelines.
Code

tests/unittest/test_bitbucket_fork_safe_secret_provider.py[1]

+"""gunicorn runs bitbucket_app with preload_app, so no client may be built at import time."""
Relevance

●●● Strong

Recent accepted test-review precedent requires imperative docstrings, directly matching this style
violation.

PR-#2703

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2694688 requires newly added docstrings/comments to use imperative phrasing. The
added module and class docstrings start with descriptive statements (gunicorn runs..., `Stands
in...`) rather than imperative verbs.

Rule 2694688: Docstrings and comments must use imperative phrasing
tests/unittest/test_bitbucket_fork_safe_secret_provider.py[1-1]
tests/unittest/test_bitbucket_fork_safe_secret_provider.py[9-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Newly added docstrings are not written in imperative mood (command form), which violates the project's docstring/comment phrasing convention.

## Issue Context
Rule requires imperative phrasing like `Return ...`, `Ensure ...`, `Build ...` rather than descriptive phrasing like `Returns ...`, `This ...`, `Stands ...`.

## Fix Focus Areas
- tests/unittest/test_bitbucket_fork_safe_secret_provider.py[1-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.

Grey Divider

Context sources
✅ Compliance rules (platform): 34 rules
Review mode: 🚀 Fast: This latest push only revises focused regression tests for a localized fork-safe secret-provider change, with no new runtime logic or independent behavior paths.

Grey Divider

Tip of the day
💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 1339c3b 🚀 Fast

Results up to commit 3b33a22 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 📜 Skill insights (0)


Action required
1. Lost startup config validation ✓ Resolved 🐞 Bug ☼ Reliability
Description
Removing the import-time get_secret_provider() call means a typo/invalid CONFIG.SECRET_PROVIDER (or
provider init failure) will no longer fail at startup, and will instead surface on first webhook
where the exception is caught and only logged, while the endpoint still returns "OK". This can leave
a seemingly healthy Bitbucket server that never processes webhooks (or intermittently fails) without
a hard startup signal.
Code

pr_agent/servers/bitbucket_app.py[30]

-secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None
+_secret_provider_state = {}
+
+
+def get_fork_safe_secret_provider():
+    """Return this process's secret provider, building it on first use after a fork."""
+    if not get_settings().get("CONFIG.SECRET_PROVIDER"):
+        return None
+    pid = os.getpid()
+    if _secret_provider_state.get("pid") != pid:
+        _secret_provider_state["provider"] = get_secret_provider()
+        _secret_provider_state["pid"] = pid
+    return _secret_provider_state["provider"]
Relevance

●●● Strong

Recent accepted GitLab precedent explicitly preserves startup validation while adding per-process
lazy rebuilding; this finding matches the intended pattern.

PR-#2550

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR removes Bitbucket’s import-time secret provider construction, which previously forced
provider validation/initialization at startup. Bitbucket now builds lazily inside
get_fork_safe_secret_provider() without any non-constructing validation, while the webhook handler
catches all exceptions and still returns "OK", masking configuration errors until runtime. GitLab’s
server shows the intended pattern: validate at import (without building) and build per PID on first
use.

pr_agent/servers/bitbucket_app.py[28-41]
pr_agent/servers/bitbucket_app.py[256-319]
pr_agent/secret_providers/init.py[7-17]
pr_agent/servers/gitlab_webhook.py[29-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`bitbucket_app.py` no longer constructs the secret provider at import (good for fork-safety), but that also removes the previous fail-fast behavior for invalid `CONFIG.SECRET_PROVIDER` values or provider initialization errors. Those errors will now happen at runtime on the first webhook, and in the `/webhook` handler they are swallowed by a broad `except`, while the endpoint returns "OK".

## Issue Context
A non-constructing validation helper already exists: `validate_secret_provider_setting()` in `pr_agent/secret_providers/__init__.py`, and GitLab’s webhook server calls it at import to preserve fail-fast behavior without building the client.

## Fix Focus Areas
- pr_agent/servers/bitbucket_app.py[19-41]
- pr_agent/secret_providers/__init__.py[7-17]
- pr_agent/servers/gitlab_webhook.py[29-33]

## Proposed change
1. Import `validate_secret_provider_setting` in `bitbucket_app.py`.
2. Call `validate_secret_provider_setting()` near module initialization (after settings are available, before request handling), mirroring `gitlab_webhook.py`.
3. Keep the lazy, PID-keyed `get_fork_safe_secret_provider()` to avoid pre-fork client creation.

This restores fast failure for invalid provider IDs while preserving the per-worker construction behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Docstrings not imperative ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
New docstrings in the added unit test are written in descriptive form (e.g., gunicorn runs...,
Stands in...) instead of imperative phrasing. This violates the requirement for imperative
docstrings/comments and reduces consistency with project style guidelines.
Code

tests/unittest/test_bitbucket_fork_safe_secret_provider.py[1]

+"""gunicorn runs bitbucket_app with preload_app, so no client may be built at import time."""
Relevance

●●● Strong

Recent accepted test-review precedent requires imperative docstrings, directly matching this style
violation.

PR-#2703

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2694688 requires newly added docstrings/comments to use imperative phrasing. The
added module and class docstrings start with descriptive statements (gunicorn runs..., `Stands
in...`) rather than imperative verbs.

Rule 2694688: Docstrings and comments must use imperative phrasing
tests/unittest/test_bitbucket_fork_safe_secret_provider.py[1-1]
tests/unittest/test_bitbucket_fork_safe_secret_provider.py[9-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Newly added docstrings are not written in imperative mood (command form), which violates the project's docstring/comment phrasing convention.

## Issue Context
Rule requires imperative phrasing like `Return ...`, `Ensure ...`, `Build ...` rather than descriptive phrasing like `Returns ...`, `This ...`, `Stands ...`.

## Fix Focus Areas
- tests/unittest/test_bitbucket_fork_safe_secret_provider.py[1-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 4e4054d ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 📜 Skill insights (0)


Remediation recommended
1. Brittle source-based assertion ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
test_the_setting_is_still_validated_at_import asserts on inspect.getsource(...) text rather than
verifying that validate_secret_provider_setting() actually runs during module import, so it can
pass even if runtime behavior regresses and can fail on harmless refactors (formatting/moving code).
This makes the regression coverage unreliable and increases test maintenance cost.
Code

tests/unittest/test_bitbucket_fork_safe_secret_provider.py[R75-80]

+def test_the_setting_is_still_validated_at_import():
+    """Keep failing at startup on a typo, which the removed import-time client used to catch."""
+    source = inspect.getsource(bitbucket_app)
+
+    assert "validate_secret_provider_setting()" in source
+
Relevance

●●● Strong

Recent accepted precedent explicitly replaces inspect.getsource assertions with behavior-based
tests, matching this finding closely.

PR-#2495

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The test currently asserts only that the string validate_secret_provider_setting() appears in the
module source, which is not equivalent to verifying the function is executed at import-time. The
production code change adds an actual import-time call, so the test should validate the behavioral
effect of that call (raising on unknown provider).

tests/unittest/test_bitbucket_fork_safe_secret_provider.py[75-80]
pr_agent/servers/bitbucket_app.py[33-34]
pr_agent/secret_providers/init.py[7-17]
PR-#2495

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`test_the_setting_is_still_validated_at_import()` currently checks for a substring in the module source via `inspect.getsource(bitbucket_app)`. This is brittle (fails on refactors/formatting) and can miss real regressions because it doesn't prove the call executes at import.

### Issue Context
The PR intentionally adds an import-time `validate_secret_provider_setting()` call in `pr_agent/servers/bitbucket_app.py`. The test should validate *observable behavior* (e.g., that an invalid `CONFIG.SECRET_PROVIDER` causes import/reload to raise), not source text.

### Fix Focus Areas
- tests/unittest/test_bitbucket_fork_safe_secret_provider.py[75-80]

### Implementation sketch
- Replace the `inspect.getsource(...)` assertion with a behavior test:
 - Monkeypatch `pr_agent.secret_providers.get_settings` (or `pr_agent.config_loader.get_settings`, depending on how you want to scope it) to return an invalid provider id.
 - Then `import importlib` and `import pr_agent.servers.bitbucket_app as bitbucket_app_module` and run `importlib.reload(bitbucket_app_module)` inside `pytest.raises(ValueError)`.
 - If needed, delete `sys.modules['pr_agent.servers.bitbucket_app']` before re-import to ensure import-time code runs.
- This directly verifies the import-time validation behavior without coupling to source layout.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread pr_agent/servers/bitbucket_app.py
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 4e4054d

…idating through an import rather than the source text
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 1339c3b

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The Bitbucket secret-provider client is built at import time, unsafe under preload_app

2 participants