Skip to content

fix: SetOrgWideDefaults._poll_action fails with stale access token after deploy#3976

Open
yippie wants to merge 3 commits into
SFDO-Tooling:mainfrom
yippie:fix/set-org-wide-defaults-stale-token
Open

fix: SetOrgWideDefaults._poll_action fails with stale access token after deploy#3976
yippie wants to merge 3 commits into
SFDO-Tooling:mainfrom
yippie:fix/set-org-wide-defaults-stale-token

Conversation

@yippie
Copy link
Copy Markdown
Contributor

@yippie yippie commented May 8, 2026

Task set_organization_wide_defaults fails 100% of the time if RTR (Refresh token rotation) is on. This is a Connected App security setting that Salesforce now requires and affects CumulusCI when embedded in MetaDeploy.

Root cause

SetOrgWideDefaults._run_task() calls _deploy(), which instantiates a new
Deploy task and invokes api() on it. Deploy.call() always calls
BaseSalesforceTask._update_credentials() → org_config.refresh_oauth_token(),
rotating org_config.access_token AFTER self.sf was already frozen in _init_task().

When _post_deploy() → _poll() → _poll_action() runs, self.sf.query() uses
the pre-deploy access token. If the org has refresh token rotation or a short
session policy, this token is invalid and every poll call returns a 401.

Sequence

Step Event org_config.access_token self.sf.session_id
SetOrgWideDefaults.call() _update_credentials() → refresh TOKEN_A
_init_task() self.sf = _init_api() TOKEN_A TOKEN_A
_deploy() → Deploy()() _update_credentials() → refresh again TOKEN_B still TOKEN_A ← stale
_post_deploy() → _poll_action() self.sf.query() TOKEN_B TOKEN_A → 401

Fix

Replace self.sf.query(...) with self.org_config.salesforce_client.query(...).

OrgConfig.salesforce_client is a @Property that constructs a fresh
simple_salesforce.Salesforce instance from self.access_token on every
access, so it always uses the current (post-deploy-refresh) token.

Tests

Updated the three _poll_action / _post_deploy tests to mock
org_config.salesforce_client via PropertyMock instead of assigning
directly to task.sf.

…ploy

The _deploy() method spawns a new Deploy task instance and calls api()
on it. Deploy.__call__() always invokes BaseSalesforceTask._update_credentials()
which calls org_config.refresh_oauth_token(), rotating the access token
stored on org_config AFTER self.sf was already frozen in _init_task().

Subsequent self.sf.query() calls in _poll_action() then use the stale
token, causing 401 failures during the sharing-enablement polling phase.

Fix: replace self.sf.query() with self.org_config.salesforce_client.query().
OrgConfig.salesforce_client is a @Property that constructs a fresh
simple_salesforce.Salesforce instance from the current org_config.access_token
on every access, so it always reflects the most recently refreshed token.

Update the three affected tests to mock org_config.salesforce_client via
PropertyMock instead of assigning to the now-unused task.sf attribute.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yippie yippie requested a review from a team as a code owner May 8, 2026 20:10
@jstvz
Copy link
Copy Markdown
Contributor

jstvz commented May 26, 2026

Thanks for the fix — the bug diagnosis is correct. After _deploy() creates a child Deploy task sharing the same org_config, the child's _update_credentials() rotates org_config.access_token, but self.sf on the parent SetOrgWideDefaults still holds the old token.

However, replacing self.sf.query(...) with self.org_config.salesforce_client.query(...) introduces a few behavioral regressions:

  1. Lost retry adapterself.sf (built by get_simple_salesforce_connection) installs an HTTPAdapter with Retry(total=5, status_forcelist=(502, 503, 504), backoff_factor=0.3). salesforce_client creates a bare Salesforce() with no retry adapter. This polling loop runs up to 600 seconds post-deploy, when transient 502/503/504 errors are common.

  2. Lost CALL_OPTS_HEADER_KEY — the connection utility sets a client=... header for server-side observability. salesforce_client does not.

  3. Object per iterationsalesforce_client is a @property that creates a new Salesforce() on every access, so each poll iteration allocates a fresh client.

Suggested alternative (one-line fix, preserves all existing behavior):

Refresh self.sf once after deploy returns. In _post_deploy, before the polling loop starts:

self.sf = self._init_api()

This re-creates the connection with the current (rotated) token while keeping the retry adapter, the CALL_OPTS_HEADER_KEY header, and the task-configured API version. Single allocation instead of one per poll iteration.

Would you be open to revising the approach? Happy to pair on it if useful.

@yippie
Copy link
Copy Markdown
Contributor Author

yippie commented May 27, 2026

@jstvz Absolutely, I think I updated per your recommendation

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.

2 participants