Skip to content

fix(change_request): run auto-apply through the public action_apply - #461

Merged
gonzalesedwin1123 merged 5 commits into
19.0from
fix/auto-apply-extension-point
Aug 28, 2026
Merged

fix(change_request): run auto-apply through the public action_apply#461
gonzalesedwin1123 merged 5 commits into
19.0from
fix/auto-apply-extension-point

Conversation

@kneckinator

Copy link
Copy Markdown
Contributor

action_apply is the extension point modules override to hang post-apply work off an apply. Auto-apply-on-approve stopped going through it, so those overrides silently stopped running.

What happened

#365 gated action_apply on the change-request manager role, because it runs the apply strategy under sudo() and is reachable over RPC. Auto-apply was then routed to the internal mechanism so the approver could be a validator rather than a manager:

# _on_approve
if self.request_type_id.auto_apply_on_approve:
    self._apply_change_request()

That solved the authorization problem but changed the contract. Any module overriding action_apply — the natural place to react to an apply — keeps working when a manager clicks Apply, and quietly does nothing when the same request is auto-applied on approval. No error, no log; only the side effects go missing.

The fix

The manager gate already exempts env.su, and says so:

"Superuser (sudo) callers and the auto-apply-on-approve path … are unaffected."

So auto-apply can use the public entry point:

-    self._apply_change_request()
+    self.sudo().action_apply()

sudo() sets superuser mode without changing the user, so applied_by_id still records the real approver — there is a test for that, since it would be an easy thing to get wrong.

What is unchanged

Both guarantees from #365 hold, and both already had tests that still pass:

  • a validator calling action_apply directly is still refused (test_validator_cannot_apply_directly);
  • approving still applies for a non-manager approver (test_auto_apply_on_approve_runs_for_non_manager_approver).

The authorization boundary for RPC callers is untouched: env.su is false there.

Trade-off

Downstream overrides now run with su=True. The apply itself already runs under sudo() via _do_apply, so this widens privilege only for post-apply side effects. The alternative — a separate _after_apply() hook called by both paths — separates authorization from extension more cleanly, but requires every consumer to move their override, which is the cost this change exists to avoid.

Tests

Two added: auto-apply goes through action_apply (spied), and the applying user is recorded as the approver rather than the superuser. Full spp_change_request_v2 suite: 397 tests, 0 failures.

Merge order

19.0.3.1.10 → 19.0.3.1.12, leaving 3.1.11 to #459. Independent of #459 and #454 in content; only the version and changelog would conflict, and only if merged out of order.

Requiring change-request manager rights to apply meant auto-apply-on-
approve was routed to the internal mechanism instead, so that the
approver could be a validator rather than a manager. But action_apply is
the extension point modules override to hang post-apply work off an
apply, and bypassing it left those overrides silently not running on
approval — no error, just missing side effects, which is the hardest
kind of regression to notice.

Auto-apply now calls action_apply under sudo(), which the manager gate
already exempts (env.su), so the authorization boundary is unchanged for
RPC callers. sudo() sets su without changing uid, so applied_by_id still
records the real approver — asserted by a test.

The two guarantees from the original change are kept and still covered:
a validator cannot call action_apply directly, and approving still
applies.
Comment thread spp_change_request_v2/models/change_request.py Fixed
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.02%. Comparing base (481cc27) to head (5a5bc47).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #461      +/-   ##
==========================================
- Coverage   76.18%   76.02%   -0.16%     
==========================================
  Files         661      627      -34     
  Lines       44109    43024    -1085     
==========================================
- Hits        33603    32709     -894     
+ Misses      10506    10315     -191     
Flag Coverage Δ
spp_api_v2_change_request 73.37% <ø> (ø)
spp_api_v2_products ?
spp_base_common 91.07% <ø> (ø)
spp_change_request_v2 78.60% <100.00%> (ø)
spp_consent ?
spp_cr_type_assign_program 92.50% <ø> (ø)
spp_dci_demo 94.28% <ø> (ø)
spp_farmer_registry_cr 61.24% <ø> (ø)
spp_farmer_registry_demo 63.39% <ø> (ø)
spp_irrigation ?
spp_mis_demo_v2 70.38% <ø> (ø)
spp_programs 66.97% <ø> (ø)
spp_registry 87.79% <ø> (ø)
spp_security 69.56% <ø> (ø)
spp_starter_sp_mis 86.84% <ø> (-2.05%) ⬇️
spp_studio_change_requests 84.85% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_change_request_v2/models/change_request.py 84.43% <100.00%> (ø)

... and 36 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The rationale is already in the comment above it — the manager gate
exempts env.su, and sudo() preserves uid so attribution is unaffected —
but semgrep's odoo-sudo-without-context needs the marker to see it.

@gonzalesedwin1123 gonzalesedwin1123 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The substance is right and I'd approve it as-is on behavior — requesting changes only for two docstrings this PR makes actively false, both trivial. Fix those and this is an approve.

Verified on the substance:

  • The contract regression is real: action_apply is the documented extension point, and routing auto-apply to _apply_change_request silently skipped every downstream override on approval. Going back through the public entry point under sudo() is the right shape, and the gate's env.su exemption was designed for exactly this.
  • No authorization widening: the strategy already ran under sudo() on the old path (_do_apply), and the workflow authorizes the apply either way. A validator still cannot call action_apply directly — the existing tests for both #365 guarantees still pass.
  • Attribution holds: sudo() keeps uid, so applied_by_id records the approver, and test_auto_apply_records_the_real_approver pins it — good instinct, that was the easy thing to get wrong.
  • The spy test correctly patches at the class, so it sees the call through the sudo'd recordset.
  • Version 19.0.3.1.12 assumes #459's 3.1.11 lands first; HISTORY will conflict textually at rebase since both edit the same region — expected for this chain, just flagging the merge-order dependency stands.

One observation, no change requested: downstream action_apply overrides now run under su on the auto-apply path but as the manager on manual apply. That's inherent to the design and the right trade (post-apply work must not fail on the approval path for ACL reasons), but override authors should know both modes exist — the updated docstring could say so.

# internal mechanism keeps ``action_apply`` the single extension
# point for apply -- downstream modules override it to hang
# post-apply work off the apply, and routing around it left those
# overrides silently not running on approval. ``sudo()`` sets

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Must fix (docs): action_apply's own docstring (line 1458, unchanged by this diff so commenting here) is now false — it still says the auto-apply-on-approve path "invokes _apply_change_request directly". After this PR it arrives at action_apply as a superuser caller. Since that paragraph documents the authorization boundary, it should describe the new reality: superuser callers are exempt, and auto-apply-on-approve is one of them (via this sudo() call, authorized by the approval workflow). Worth adding a line that overrides of action_apply therefore run under su on the auto-apply path and as the clicking manager on the manual path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 35d4103. The paragraph now says superuser (env.su) callers are exempt and that auto-apply-on-approve is one of them, reaching action_apply through sudo() from _on_approve, authorized by the approval workflow — so the approver may be a validator rather than a manager.

Added your suggested point too: action_apply is the extension point, so an override runs on both paths — under su when auto-applied on approval, and as the manager who clicked Apply on the manual path. Noted alongside it that sudo() sets su without changing uid, so applied_by_id records the approver rather than the superuser (there is a test pinning that).


def test_auto_apply_on_approve_runs_for_non_manager_approver(self):
"""Auto-apply-on-approve must still work when the approver is a
validator (not a manager): _on_approve routes through the internal

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Must fix (docs): stale — _on_approve no longer routes through the ungated internal mechanism; it goes through the public action_apply under sudo(), which the manager gate exempts. The test's assertion is still exactly right (a validator approver must work); only the explanation of why has changed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 35d4103. Rewritten to say _on_approve reaches the public action_apply through sudo(), and the manager gate exempts superuser callers.

Agreed the assertion itself was already correct — only the stated reason had gone stale, which is the kind of thing that survives indefinitely because nothing fails when it drifts.

Two explanations were left describing the behaviour this branch replaced,
both flagged in review.

action_apply's docstring still said auto-apply-on-approve "invokes
_apply_change_request directly". It now arrives at action_apply as a
superuser caller instead, and since that paragraph documents the
authorization boundary it has to describe what actually happens. Also
notes that action_apply is the extension point, so an override runs
under su on the auto-apply path and as the clicking manager on the
manual one, with applied_by_id recording the approver either way.

The docstring on test_auto_apply_on_approve_runs_for_non_manager_approver
said _on_approve routes through the ungated internal mechanism. The
assertion it makes is unchanged and still right — a validator approver
must work — but the reason is now that the manager gate exempts
superuser callers.

Documentation only; no behaviour change.

@gonzalesedwin1123 gonzalesedwin1123 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving — both docstrings now describe the real mechanism, and the action_apply one additionally documents the two execution modes for override authors (su on auto-apply, the clicking manager on manual) with the uid-preservation guarantee spelled out. That's exactly the paragraph a downstream developer needs. CI fully green.

Merge order reminder: after #459 (3.1.11), this takes 3.1.12; expect the same squash-induced HISTORY/manifest conflict #471 just had — mechanical to resolve.

#459 was squash-merged as 481cc27, conflicting with this branch in the
version line, HISTORY, and the generated README/index.html. Resolved to
19.0.3.1.12 with both HISTORY entries stacked (3.1.12 over 3.1.11), and
the README/index.html regenerated from the resolved fragments with the
repo's pinned oca-gen-addon-readme hook.
The local oca-gen-addon-readme rendering differs from CI's pinned env in
RST table column widths. Applied CI's printed expected diff verbatim;
both files now match CI's blob hashes (99b63c9, c8d57bc).
@gonzalesedwin1123
gonzalesedwin1123 merged commit ac0e38e into 19.0 Aug 28, 2026
34 checks passed
@gonzalesedwin1123
gonzalesedwin1123 deleted the fix/auto-apply-extension-point branch August 28, 2026 05:17
gonzalesedwin1123 added a commit that referenced this pull request Aug 28, 2026
#459 and #461 were squash-merged (481cc27, ac0e38e), conflicting with
this branch in the version line, HISTORY, tests/__init__.py, and the
generated README/index.html. Resolved to 19.0.3.1.13 with the HISTORY
entries stacked (3.1.13 over 3.1.12 over 3.1.11), both test imports
kept, and README/index.html regenerated from the resolved fragments
with CI's table-width rendering applied.
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.

3 participants