Add an audit event for CI runner tag (tag_list) changes

Problem

Changing a runner's tags (tag_list) produces no audit event. There is no entry in the audit event types, nothing in instance/group/project audit events, and nothing retrievable via the Audit Events API or streaming.

For organizations that gate CI on a controlled tag set (for example, only private runners with specific tags are permitted), a tag silently being added to or removed from a runner is a compliance and security blind spot: there is no first-party record of who changed it, what changed, or when.

Why this matters for GitLab Duo

GitLab Duo Agent Platform flows run as CI/CD jobs, and only on runners tagged gitlab--duo (Configure runners to execute flows). Adding or removing that tag decides which runners execute Duo flows and receive their credentials.

As more organizations adopt GitLab Duo, many need a strict audit trail of where AI tools run. Today a change to the gitlab--duo tag leaves no record, so an organization can't show who moved Duo workloads onto or off a runner, or when.

Scope (intentionally minimal)

Audit tag_list changes only — adding or removing runner tags.

Other mutable runner settings (locked, access_level, run_untagged, maximum_timeout, paused, description) and the shared-runner enable/override group and project toggles are out of scope for this issue. An existing separate proposal already covers the shared-runner configuration toggles, and the remaining per-runner settings can follow this same pattern later. Keeping this issue to tags alone makes it a small, self-contained change.

Current coverage vs. the gap

Runner lifecycle events are already audited via AuditEvents::RunnerAuditEventService (ci_runner_registered, ci_runner_unregistered, create, assign/unassign, set-projects, bulk-delete), and runner controller updates were audited in !223435 (merged).

However, Ci::Runners::UpdateRunnerService — the code path that edits tag_list — emits no audit event. There is no ee/app/services/ee/ci/runners/update_runner_service.rb and no corresponding ci_runner_*_updated audit event type.

Proposal

Emit one audit event (for example ci_runner_tags_updated) when tag_list changes during a runner update, including the before/after tag sets (or added/removed tags) in the event details. This should work for instance, group, and project runners and flow to the Audit Events API and streaming destinations like the other runner events.

Implementation sketch & difficulty — Low

  • Hook point: app/services/ci/runners/update_runner_service.rb#execute (CE) runs runner.update(params). Add an EE override ee/app/services/ee/ci/runners/update_runner_service.rb that wraps super with the same super.tap { |result| ... } shape used by its sibling services in ee/app/services/ee/ci/runners/.
  • Detect the change: capture tag_list before calling super and compare to the runner's tags after a successful update. Tags come from acts-as-taggable, so prefer a snapshot-before/compare-after (or tag_list_changed?) rather than relying on runner.previous_changes. This mirrors how ee/app/services/ci/runner_controllers/update_service.rb snapshots old_state before the update.
  • Emit via the existing AuditEvents::RunnerAuditEventService with a new name ci_runner_tags_updated and a message such as "Changed CI runner tags", carrying the added/removed tags in additional_details.
  • Define the event type: add ee/config/audit_events/types/ci_runner_tags_updated.yml (~10 lines), mirroring ee/config/audit_events/types/ci_runner_registered.yml.
  • Tests + docs: a service spec plus an API request spec asserting the event is emitted. doc/user/compliance/audit_event_types.md is generated from the YAML definition.

Closest template: !223435 (merged) ("Introduce runner controller audit events"). Its runner_controller_updated slice — a service audit method, one 10-line event-type YAML, a service spec, and generated docs — is the shape to copy. This change is smaller because AuditEvents::RunnerAuditEventService already exists and only a single attribute is in scope.

Estimated footprint: ~1 small EE service file, 1 event-type YAML, 1–2 spec files, and regenerated docs.

Edited by Cleveland Bledsoe Jr