Skip to content

Re-resolve cached scan credentials after a TTL - #1561

Open
ysmaoui wants to merge 3 commits into
jenkinsci:masterfrom
ysmaoui:fix/scm-source-credentials-ttl
Open

Re-resolve cached scan credentials after a TTL#1561
ysmaoui wants to merge 3 commits into
jenkinsci:masterfrom
ysmaoui:fix/scm-source-credentials-ttl

Conversation

@ysmaoui

@ysmaoui ysmaoui commented Sep 7, 2026

Copy link
Copy Markdown

Description

GitHubSCMSource and GitHubSCMNavigator cache the credentials object they resolved in a transient volatile field and only resolve it again when the caller asks for a refresh. Only the full-scan paths do that (GitHubSCMSource#retrieve when the observer is not SCMHeadObserver.Any, and GitHubSCMNavigator#visitSources); every per-build and per-head path passes forceRefresh=false.

The field has no expiry, so its lifetime is the source or navigator object: until the job configuration is saved or Jenkins restarts. A credential whose content changed under the same ID is therefore never picked up by builds on its own. A rescan is the only routine operation that clears it.

This is most visible with credentials backed by an external store (Azure Key Vault, HashiCorp Vault, AWS Secrets Manager), where refreshing the provider replaces the credentials object wholesale. Rotating a GitHub App there and reloading the provider cache leaves builds authenticating as the old app, with no indication that anything is stale: the credentials page shows the new value while builds keep using the old one. It also applies to plain stored credentials, where editing a secret in the UI creates a new object that cached references never see.

This bounds the reuse instead of removing it. Connector.credentialsTtlMillis defaults to 60 seconds and is configurable through the system property org.jenkinsci.plugins.github_branch_source.Connector.credentialsTtlSeconds; 0 resolves on every use, a negative value reuses indefinitely and so restores today's behaviour exactly. During a scan that means a handful of lookups per source rather than one per branch or PR, so the throughput win from [JENKINS-73172] / #787 is kept, while staleness is capped at a minute instead of unbounded.

Two notes for reviewers of #787 in particular:

  • Both getCredentials methods stay private, per the review nits on that PR. The test reaches them by reflection rather than widening them; happy to swap that for package-private visibility if you prefer.
  • It also bounds the case raised in this comment: the cached reference is keyed on nothing, so a change to apiUri, credentialsId or repoOwner on a live source is not noticed either. A TTL is not the keyed lookup suggested there, but it does put a bound on it.
  • For credentials whose provider returns a fresh object per lookup, re-resolving discards the per-instance AppInstallationToken cache in GitHubAppCredentials, so a new installation token is minted at most once per TTL per source. Providers that cache internally (Azure Key Vault, HashiCorp Vault) return a stable object and are unaffected. Raising the default is easy if you think that trade is wrong.

Submitter checklist

  • Link to JIRA ticket in description, if appropriate.
  • Change is code complete and matches issue description
  • Automated tests have been added to exercise the changes
  • Reviewer's manual test instructions provided in PR description. See Reviewer's first task below.

No JIRA ticket filed yet, happy to open one if maintainers prefer that before review.

CredentialsRefreshTest covers both classes: resolve, replace the stored credential under the same ID, assert the cached object is still reused inside the TTL, then assert the new one is resolved once a real TTL has elapsed. Both fail without the production change. A third test asserts the isCredentialsStale contract directly, including the zero and negative settings.

Manual test:

  1. Create a username/password credential github and a multibranch project using it.
  2. Scan it, then build a branch.
  3. Change the password on that credential.
  4. Rebuild the same branch within a minute: the old password is still used, as before.
  5. Rebuild after a minute: the new password is used. On master, it stays stale until a rescan.

Reviewer checklist

  • Run the changes and verify that the change matches the issue description
  • Reviewed the code
  • Verified that the appropriate tests have been written or valid explanation given

Documentation changes

  • No user-facing documentation change. The system property is an escape hatch for operators who want stricter or looser behaviour than the default.

Users/aliases to notify

@Dohbedoh, as the author of #787.

GitHubSCMSource and GitHubSCMNavigator keep the credentials object they
resolved and only re-resolve it when a full scan asks them to. Every
per-build path passes forceRefresh=false, so a credential whose content
changed is picked up only after a rescan, a config save or a restart.

Bound the reuse to 60s, configurable through
org.jenkinsci.plugins.github_branch_source.Connector.credentialsTtlSeconds,
0 to resolve on every use.
@ysmaoui
ysmaoui requested a review from a team as a code owner September 7, 2026 08:46
Both getCredentials methods stay private, as asked for in the review of
jenkinsci#787; the test reaches them by reflection instead.

The tests forced a refresh by setting the TTL to 0, which short-circuits
before the clock comparison, leaving it uncovered. They now let a real
TTL elapse, and the contract itself is asserted directly.

A negative TTL now reuses the credentials object indefinitely, an exact
opt-out for anyone who wants the previous behaviour.
A scan passing forceRefresh=true must resolve even inside the TTL, and a
negative credentialsTtlSeconds must map to no expiry. Neither was
asserted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant