Scope auto-merge abort notes to the target project
What does this MR do and why?
The bug
MergeRequests::BaseService#abort_auto_merge (in app/services/merge_requests/base_service.rb) built the auto-merge service from the calling service's own project:
AutoMergeService.new(project, current_user).abort(merge_request, reason)That project is the project whose push or lifecycle event is being processed. MergeRequests::RefreshService is deliberately constructed with the project that was pushed to, and it finds affected merge requests through the source side (@project.source_of_merge_requests, a source_project_id foreign key). So for a merge request opened from a fork, that project is the fork.
The project then travels down into the system note. AutoMergeService#abort copies it into the strategy instance, the strategy passes it to SystemNoteService, and NoteSummary writes it into the note's project attribute alongside the noteable.
Note has a validation (in app/models/note.rb) that rejects exactly this:
unless note.noteable.try(:project) == note.project
errors.add(:project, 'does not match noteable project')
endMergeRequest#project is an alias for target_project. So the note's noteable.project is the upstream project while its own project is the fork, the validation fails, and the note is invalid.
Because SystemNotes::BaseService#create_note calls Note.create rather than Note.create!, nothing raised and no insert was issued. The note simply disappeared. The abort itself always succeeded; only the note explaining it was lost.
The user-visible effect: on a fork merge request, auto-merge gets switched off by a push with no note in the activity feed explaining why.
How it was found
The instrumentation added in !251536 (merged) logs a SystemNotes::BaseService::UnpersistedSystemNoteError whenever a system note fails to persist. Searching the pubsub-sidekiq-inf-gprd* data view in Kibana for json.exception.class: "SystemNotes::BaseService::UnpersistedSystemNoteError" over five days returned 62 records. The dominant signature was note_action: merge with note_errors: ["Project does not match noteable project"], and json.meta.project on those records was the fork rather than the upstream project.
Two production examples
Both are public merge requests where the note is missing from the activity feed.
- !252518 (merged) - source project is a fork, target is
gitlab-org/gitlab. The log line is at 2026-09-03 14:20:32.271 UTC. The missing note is "aborted automatic add to merge train because the source branch was updated." The feed shows auto-merge enabled at 13:13:18, then a push at 14:20:32, then the author re-enabling auto-merge at 14:46:14 with nothing in between explaining the interruption. - pixels-online-oss/pixels/oss_pixels_server!27 (merged) - source project is a fork, target is
pixels-online-oss/pixels/oss_pixels_server. The log line is at 2026-09-03 13:13:46.319 UTC. The missing note is "aborted the automatic merge because the source branch was updated." Auto-merge was enabled on 2026-09-02 at 14:29:52, the push landed at 13:13:46, and the author re-enabled auto-merge at 13:19:42.
In both cases the "added N commits" note persisted between 70 and 99 milliseconds after the dropped note, in the same RefreshService run on the same merge request. That note is created by notify_about_push, which passes merge_request.project. Same job, same merge request, and the only difference is which project the note was filed under.
The fix
Pass merge_request.target_project instead, behind a feature flag.
Three call paths reach the defective line, and all are fixed by it:
MergeRequests::RefreshService#abort_auto_merges, when a fork's source branch is pushed. This is the dominant source in production.Projects::UnlinkForkServicecallingMergeRequests::CloseService, when a fork is unlinked or deleted.MergeRequests::PostMergeService#cancel_auto_merges_targeting_source_branch, which has the reverse geometry: the merge request lives in the fork while the calling service holds the upstream project. Deriving from the merge request is correct in that direction too.
The same file already does this for MergeRequests::CloseService (project: mr.target_project), and the REST API already constructs AutoMergeService.new(merge_request.target_project, ...) in two places. So the fix aligns this call site with existing practice rather than introducing a new pattern.
Why this is low risk
Every use of the calling service's project inside the whole AutoMerge chain is a system note container. AutoMerge::BaseService contains exactly one reference to a project, and it is already merge_request.project. The work the abort actually performs, clearing the auto-merge parameters and saving the merge request, never reads a project at all.
There are no Feature.enabled? and no can? calls anywhere in the AutoMerge chain, so the change alters neither feature flag actors nor authorization. All three merge train capability checks already read merge_request.project, so licensing decisions were already keyed on the target project and are unaffected.
merge_requests.target_project_id is NOT NULL, so the new value can never be nil. source_project_id is nullable, which is an additional argument for deriving from the target: the old code could pass a project that is in the middle of being deleted, which is exactly the fork unlink path.
Feature flag
- Name:
auto_merge_abort_uses_target_project - Type:
gitlab_com_derisk, default disabled - Actor:
merge_request.target_project
When the flag is disabled, behavior is byte for byte what it is today. No changelog entry is included because the flag is disabled by default.
Test coverage
A new describe block in spec/services/merge_requests/refresh_service_spec.rb sets up a fork merge request with auto-merge enabled, runs RefreshService with the fork as the project, and asserts:
- With the flag enabled, the abort note is persisted and its
projectis the target project. - With the flag enabled, the auto-merge is still aborted.
- With the flag disabled, no abort note is persisted.
The flag-disabled example reproduces the bug in the test suite, so the spec fails against the old behavior by construction.
References
Screenshots or screen recordings
There is no UI change here; the change affects a system note that appears in the merge request activity feed, and the two production examples linked in the References section above show where that note is currently missing.
How to set up and validate locally
Local verification without FF
Also logs could be found locally matching exactly what we see from prod logs
Click to expand
{"severity":"ERROR","time":"2026-09-07T06:22:42.733Z","correlation_id":"01M1X8H0FJQHHBVW3ZWV49B4GR","meta.caller_id":"UpdateMergeRequestsWorker","meta.remote_ip":"172.16.123.1","meta.feature_category":"code_review_workflow","meta.http_router_rule_action":"proxy","meta.user":"root","meta.gl_user_id":1,"meta.project":"gnuwget/test-owner","meta.root_namespace":"gnuwget","meta.client_id":"user/1","meta.organization_id":1,"meta.root_caller_id":"POST /api/:version/internal/post_receive","exception.class":"SystemNotes::BaseService::UnpersistedSystemNoteError","exception.message":"System note was not persisted","exception.backtrace":["lib/gitlab/error_tracking.rb:93:in `track_exception'","app/services/system_notes/base_service.rb:58:in `track_unpersisted_note'","app/services/system_notes/base_service.rb:25:in `block in create_note'","\u003cinternal:kernel\u003e:90:in `tap'","app/services/system_notes/base_service.rb:24:in `create_note'","app/services/system_notes/merge_requests_service.rb:26:in `abort_auto_merge'","app/services/system_note_service.rb:175:in `abort_auto_merge'","app/services/auto_merge/merge_when_checks_pass_service.rb:36:in `block in abort'","app/services/auto_merge/base_service.rb:117:in `block in clear_auto_merge'","app/models/concerns/cross_database_modification.rb:91:in `block in transaction'","activerecord (7.2.3.1) lib/active_record/connection_adapters/abstract/transaction.rb:616:in `block in within_new_transaction'","activesupport (7.2.3.1) lib/active_support/concurrency/null_lock.rb:9:in `synchronize'","activerecord (7.2.3.1) lib/active_record/connection_adapters/abstract/transaction.rb:613:in `within_new_transaction'","activerecord (7.2.3.1) lib/active_record/connection_adapters/abstract/database_statements.rb:361:in `transaction'","activerecord (7.2.3.1) lib/active_record/transactions.rb:234:in `block in transaction'","activerecord (7.2.3.1) lib/active_record/transactions.rb:233:in `transaction'","lib/gitlab/database.rb:409:in `transaction'","app/models/concerns/cross_database_modification.rb:82:in `transaction'","app/services/auto_merge/base_service.rb:115:in `clear_auto_merge'","app/services/auto_merge/base_service.rb:49:in `abort'","app/services/auto_merge/merge_when_checks_pass_service.rb:35:in `abort'","app/services/auto_merge_service.rb:66:in `block in abort'","app/services/auto_merge_service.rb:93:in `perform_method'","app/services/auto_merge_service.rb:65:in `abort'","app/services/merge_requests/base_service.rb:273:in `abort_auto_merge'","app/services/merge_requests/refresh_service.rb:312:in `abort_auto_merges'","app/services/merge_requests/refresh_service.rb:41:in `block in refresh_merge_requests!'","app/services/merge_requests/refresh_service.rb:39:in `each'","app/services/merge_requests/refresh_service.rb:39:in `refresh_merge_requests!'","ee/app/services/ee/merge_requests/refresh_service.rb:14:in `refresh_merge_requests!'","app/services/merge_requests/refresh_service.rb:21:in `execute'","app/workers/update_merge_requests_worker.rb:30:in `perform'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:220:in `execute_job'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:185:in `block (4 levels) in process'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:180:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","ee/lib/gitlab/sidekiq_middleware/set_session/server.rb:21:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/current_organization/server.rb:25:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/identity/restore.rb:12:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/resource_usage_limit/middleware.rb:16:in `perform'","lib/gitlab/sidekiq_middleware/resource_usage_limit/server.rb:8:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/skip_jobs.rb:60:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/throttling/middleware.rb:18:in `perform'","lib/gitlab/sidekiq_middleware/throttling/server.rb:8:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/pause_control/strategies/base.rb:31:in `perform'","lib/gitlab/sidekiq_middleware/pause_control/strategy_handler.rb:22:in `perform'","lib/gitlab/sidekiq_middleware/pause_control/server.rb:8:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executing.rb:16:in `perform'","lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb:44:in `perform'","lib/gitlab/sidekiq_middleware/duplicate_jobs/server.rb:8:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/concurrency_limit/middleware.rb:44:in `perform'","lib/gitlab/sidekiq_middleware/concurrency_limit/server.rb:8:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/click_house/migration_support/sidekiq_middleware.rb:7:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/worker_context.rb:9:in `wrap_in_optional_context'","lib/gitlab/sidekiq_middleware/worker_context/server.rb:19:in `block in call'","lib/gitlab/application_context.rb:190:in `block in use'","gitlab-labkit (5.0.0) lib/labkit/context.rb:43:in `with_context'","lib/gitlab/application_context.rb:190:in `use'","lib/gitlab/application_context.rb:110:in `with_context'","lib/gitlab/sidekiq_middleware/worker_context/server.rb:17:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_status/server_middleware.rb:7:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_versioning/middleware.rb:9:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/query_analyzer.rb:7:in `block in call'","lib/gitlab/database/query_analyzer.rb:108:in `within'","lib/gitlab/sidekiq_middleware/query_analyzer.rb:7:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/admin_mode/server.rb:14:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/set_ip_address.rb:7:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/query_logs.rb:14:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/instrumentation_logger.rb:10:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/batch_loader.rb:7:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/extra_done_log_metadata.rb:7:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/server_metrics.rb:115:in `block in call'","lib/gitlab/sidekiq_middleware/server_metrics.rb:144:in `block in instrument'","lib/gitlab/metrics/background_transaction.rb:33:in `run'","lib/gitlab/sidekiq_middleware/server_metrics.rb:144:in `instrument'","lib/gitlab/sidekiq_middleware/server_metrics.rb:114:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/query_limiting/sidekiq_middleware.rb:12:in `block in call'","lib/gitlab/query_limiting/transaction.rb:48:in `run'","lib/gitlab/query_limiting/sidekiq_middleware.rb:11:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/request_store_middleware.rb:8:in `block in call'","gems/gitlab-safe_request_store/lib/gitlab/safe_request_store.rb:66:in `enabling_request_store'","gems/gitlab-safe_request_store/lib/gitlab/safe_request_store.rb:59:in `ensure_request_store'","lib/gitlab/sidekiq_middleware/request_store_middleware.rb:7:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","gitlab-labkit (5.0.0) lib/labkit/middleware/sidekiq/server.rb:22:in `block in call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:180:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","gitlab-labkit (5.0.0) lib/labkit/middleware/sidekiq/user_experience_sli/server.rb:16:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","gitlab-labkit (5.0.0) lib/labkit/middleware/sidekiq/context/server.rb:16:in `block in call'","gitlab-labkit (5.0.0) lib/labkit/context.rb:43:in `with_context'","gitlab-labkit (5.0.0) lib/labkit/middleware/sidekiq/context/server.rb:15:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:173:in `invoke'","gitlab-labkit (5.0.0) lib/labkit/middleware/sidekiq/server.rb:21:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/monitor.rb:10:in `block in call'","lib/gitlab/sidekiq_daemon/monitor.rb:46:in `within_job'","lib/gitlab/sidekiq_middleware/monitor.rb:9:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/shard_awareness_validator.rb:10:in `block in call'","lib/gitlab/sidekiq_sharding/validator.rb:42:in `enabled'","lib/gitlab/sidekiq_middleware/shard_awareness_validator.rb:9:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","lib/gitlab/sidekiq_middleware/size_limiter/server.rb:13:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","sentry-sidekiq (5.23.0) lib/sentry/sidekiq/sentry_context_middleware.rb:54:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","vendor/gems/sidekiq/lib/sidekiq/job/interrupt_handler.rb:9:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'","vendor/gems/sidekiq/lib/sidekiq/metrics/tracking.rb:26:in `track'","vendor/gems/sidekiq/lib/sidekiq/metrics/tracking.rb:134:in `call'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:182:in `traverse'","vendor/gems/sidekiq/lib/sidekiq/middleware/chain.rb:173:in `invoke'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:184:in `block (3 levels) in process'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:145:in `block (6 levels) in dispatch'","vendor/gems/sidekiq/lib/sidekiq/job_retry.rb:118:in `local'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:144:in `block (5 levels) in dispatch'","vendor/gems/sidekiq/lib/sidekiq/rails.rb:27:in `block in call'","activesupport (7.2.3.1) lib/active_support/reloader.rb:77:in `block in wrap'","activesupport (7.2.3.1) lib/active_support/execution_wrapper.rb:91:in `wrap'","activesupport (7.2.3.1) lib/active_support/reloader.rb:74:in `wrap'","vendor/gems/sidekiq/lib/sidekiq/rails.rb:26:in `call'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:139:in `block (4 levels) in dispatch'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:281:in `stats'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:134:in `block (3 levels) in dispatch'","lib/gitlab/sidekiq_logging/structured_logger.rb:21:in `call'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:133:in `block (2 levels) in dispatch'","vendor/gems/sidekiq/lib/sidekiq/job_retry.rb:85:in `global'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:132:in `block in dispatch'","vendor/gems/sidekiq/lib/sidekiq/job_logger.rb:40:in `prepare'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:131:in `dispatch'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:183:in `block (2 levels) in process'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:182:in `handle_interrupt'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:182:in `block in process'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:181:in `handle_interrupt'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:181:in `process'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:86:in `process_one'","vendor/gems/sidekiq/lib/sidekiq/processor.rb:76:in `run'","vendor/gems/sidekiq/lib/sidekiq/component.rb:10:in `watchdog'","vendor/gems/sidekiq/lib/sidekiq/component.rb:19:in `block in safe_thread'"],"user.username":"root","tags.queue":"default","tags.jid":"bf1cabda432f2fd5dca4c83b","tags.program":"sidekiq","tags.locale":"en","tags.feature_category":"code_review_workflow","tags.correlation_id":"01M1X8H0FJQHHBVW3ZWV49B4GR","extra.noteable_type":"MergeRequest","extra.noteable_id":202,"extra.commit_id":null,"extra.note_action":"merge","extra.note_errors":["Project does not match noteable project"],"extra.note_bytesize":180}
After turning on FF
local steps
Manual verification — no code editing needed
The flag is default_enabled: false, so the branch already contains both behaviours. Flip the flag, don't edit code.
Setup
- git checkout zl/614048-fork-auto-merge-abort-note, GDK up (Sidekiq included — RefreshService runs inside UpdateMergeRequestsWorker).
- Fork any project that has a repository, via the UI Fork button. Console alternative: Projects::ForkService.new(upstream, user, namespace: user.namespace).execute[:project] (returns a ServiceResponse).
- Open an MR from the fork's branch into the upstream project.
Arm auto-merge — do this in rails console rather than the UI. The "Set to auto-merge" button only appears when the MR is not mergeable or has a pipeline in progress (merge_when_checks_pass_service.rb:46), which a clean GDK MR isn't:
mr = MergeRequest.find() mr.update!( auto_merge_enabled: true, auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS, merge_user: mr.author ) mr.merge_params[:sha] = mr.diff_head_sha # so the next push looks stale mr.save!
abort_auto_merges? reads this with with_indifferent_access, so either key form works.
Round 1 — reproduce the bug (flag off, the default)
Feature.disabled?(:auto_merge_abort_uses_target_project) # => true
Now push to the fork's source branch. Easiest without cloning: edit any file on that branch through the fork's UI.
Expect:
- Feed shows "added 1 commit" but no abort note.
- mr.reload.auto_merge_enabled is false — the abort worked, only the note vanished.
- A log line appears:
grep UnpersistedSystemNoteError log/exceptions_json.log | tail -1 | jq '{class: ."exception.class", errors: ."extra.note_errors", action: ."extra.note_action", bytes: ."extra.note_bytesize"}'
{ "class": "SystemNotes::BaseService::UnpersistedSystemNoteError", "errors": ["Project does not match noteable project"], "action": "merge", "bytes": 177 }
Round 2 — see it fixed
Feature.enable(:auto_merge_abort_uses_target_project)
Re-arm auto-merge (round 1 turned it off — repeat the console block), then push again.
Expect:
- Feed shows the note, immediately above "added 1 commit": ▎ aborted the automatic merge because the source branch was updated. Learn more
- No new UnpersistedSystemNoteError line.
- mr.notes.last.project_id == mr.target_project_id.
Faster alternative, if you'd rather skip the fork/UI dance
Everything above collapses into one console call that hits the same code path:
MergeRequests::RefreshService .new(project: mr.source_project, current_user: mr.author) .execute(oldrev, newrev, "refs/heads/#{mr.source_branch}")
project: must be source_project — that's the whole bug. Passing target_project there would mask it.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

