Approval Rules Webhook Payload Shows ActiveRecord Proxy Instead of Array
Summary
When updating a merge request without changing approval rules, the webhook payload incorrectly serializes the approval_rules.previous value as an ActiveRecord association proxy object string instead of properly converting it to an array.
Example of Incorrect Behavior
{
"approval_rules": {
"previous": "#<ApprovalMergeRequestRule::ActiveRecord_Associations_CollectionProxy:0x0000000379856f48>",
"current": [
{
"id": 1,
"approvals_required": 0,
"name": "All Members",
"rule_type": "any_approver",
// ... other fields
}
]
}
}
Expected Behavior
The previous value should be an array of approval rule objects (or an empty array if there were no previous approval rules):
{
"approval_rules": {
"previous": [
{
"id": 1,
"approvals_required": 0,
"name": "All Members",
"rule_type": "any_approver",
// ... other fields
}
],
"current": [
{
"id": 1,
"approvals_required": 0,
"name": "All Members",
"rule_type": "any_approver",
// ... other fields
}
]
}
}
Steps to Reproduce
- Create a merge request with approval rules
- Update the merge request via the side panel (e.g., change title, description, or any field)
- Do NOT change the approval rules
- Observe the webhook payload
Root Cause
The bug is in ee/app/models/concerns/ee/issuable.rb in the old_approval_rules method:
def old_approval_rules(assoc)
@_old_approval_rules ||= assoc.fetch(:approval_rules, approval_rules)
end
The fallback approval_rules returns an ActiveRecord association proxy, not a converted array.
When hook_association_changes compares:
if old_approval_rules(old_associations) != approval_rules_hook_attributes
changes[:approval_rules] = [old_approval_rules(old_associations), approval_rules_hook_attributes]
end
If :approval_rules is missing from old_associations, the fallback returns the raw proxy, which then gets included in the webhook payload.
Proposed Fix
Update the old_approval_rules method in ee/app/models/concerns/ee/issuable.rb to ensure the fallback returns converted data:
def old_approval_rules(assoc)
@_old_approval_rules ||= assoc.fetch(:approval_rules, approval_rules.map(&:hook_attrs))
end
This ensures that even when the fallback is used, it returns properly formatted approval rule data instead of an ActiveRecord proxy.
Similar Pattern Fixed for Reviewers
This bug follows the exact same pattern that was recently fixed for reviewers in #XXXXX (reference the MR for reviewer webhooks). The reviewer fix:
- Simplified the fallback logic in
build_standard_reviewer_changesto usefetch(:reviewers_hook_attrs, []) - Ensured
IssuableBaseService#associations_before_updatealways capturesreviewers_hook_attrs
For approval rules, the same approach should be taken - the EE service override already captures the data correctly:
# ee/app/services/ee/issuable_base_service.rb
def associations_before_update(issuable)
associations = super
associations[:approval_rules] = issuable.approval_rules.map(&:hook_attrs) if issuable.supports_approval_rules?
# ...
end
So the fallback should just return an empty array or already-converted data.
Impact
- Webhook consumers receiving approval rule changes get malformed data
- The
previousvalue cannot be parsed as JSON - This breaks any automation or integrations that process approval rule changes
- Only affects EE instances with approval rules enabled