Rémy Coutable activity https://gitlab.com/rymai 2026-03-19T17:01:57Z tag:gitlab.com,2026-03-19:5223186134 Rémy Coutable commented on merge request !228096 at GitLab.org / GitLab 2026-03-19T17:01:57Z rymai Rémy Coutable

The MR is empty.

tag:gitlab.com,2026-03-19:5223185023 Rémy Coutable closed merge request !228096: Update dependency ruby-lsp-rspec to v0.1.29 at GitLab.org / GitLab 2026-03-19T17:01:41Z rymai Rémy Coutable

This MR contains the following updates:

Package Update Change MyDiffEnd
ruby-lsp-rspec (changelog) patch 0.1.28 -> 0.1.29 https://my.diffend.io/gems/ruby-lsp-rspec/0.1.28/0.1.29

MR created with the help of gitlab-org/frontend/renovate-gitlab-bot


Release Notes

st0012/ruby-lsp-rspec (ruby-lsp-rspec)

v0.1.29

Compare Source

What's Changed

Enhancements
🐛 Bug Fixes
🛠️ Other Changes

Full Changelog: https://github.com/st0012/ruby-lsp-rspec/compare/v0.1.28...v0.1.29


Configuration

📅 Schedule: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻️ Rebasing: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this MR and you won't be reminded about this update again.


  • If you want to rebase/retry this MR, check this box

This MR has been generated by Renovate Bot.

tag:gitlab.com,2026-03-19:5223180037 Rémy Coutable commented on merge request !308 at GitLab.org / ruby / gems / GitLab Dangerfiles 2026-03-19T17:00:33Z rymai Rémy Coutable

@andrewn Sorry, I think I assumed it required more reviews since there were other reviewers assigned.

tag:gitlab.com,2026-03-19:5223160830 Rémy Coutable opened issue #594163: Add ability to delete an Organization at GitLab.org / GitLab 2026-03-19T16:56:13Z rymai Rémy Coutable

Context

As part of the TLG-to-Organization onboarding flow, we want to begin auto-transferring Top-Level Groups (TLGs) to Organizations. Once in place, customers will be able to combine multiple TLGs into a single Organization via an onboarding experience.

We already have tooling to transfer groups from one Organization to another. However, there is currently no way to delete an Organization — which is required when merging TLGs: after transferring all groups out of a redundant Organization, that Organization needs to be removed.

Proposal

Add the ability to soft-delete an Organization, reusing the same soft-delete pattern already used for groups.

Behavior

  • Visibility: a soft-deleted Organization should appear as non-existent to all users except admins.
  • Reverting: soft-deletion can only be reverted by admins or via a Rails console. No dedicated restoration UI is required.
  • Eligibility: soft-deletion is only allowed when the Organization is fully empty — no groups, no projects, and no org-level entities (e.g. artifacts from the upcoming org-level Artifact Registry).
  • Roles: Organization owners and instance admins can trigger soft-deletion.

Implementation plan

Organizations are not namespace-hierarchical (no parent/ancestors), so the state machine is simpler than Namespaces::Stateful. There is no ancestor_inherited state, no archival, and no state preservation needed (cancel always returns to active).

States

Value Name Description
0 active Default. Organization is fully operational.
1 deletion_scheduled Soft-deleted. Invisible to non-admins. Reversible.
2 deletion_in_progress Hard-deletion worker is running (reserved for future).

Events & Transitions

Event From To Notes
schedule_deletion active deletion_scheduled Soft-delete (issue scope)
start_deletion active, deletion_scheduled deletion_in_progress Future: hard-delete worker
cancel_deletion deletion_scheduled, deletion_in_progress active Restore
reschedule_deletion deletion_in_progress deletion_scheduled Future: retry on failure

State transition diagram

active ──schedule_deletion──> deletion_scheduled ──start_deletion──> deletion_in_progress
  ^                                  |                                         |
  └──────── cancel_deletion ─────────┘                                         |
  ^                                                                            |
  └──────────────────────── cancel_deletion / reschedule_deletion ─────────────┘

Phase 1: Database Migrations

1a. Add state to organizations

New file: db/migrate/<ts>_add_state_to_organizations.rb

class AddStateToOrganizations < Gitlab::Database::Migration[2.3]
  def change
    add_column :organizations, :state, :smallint, null: false, default: 0
    add_index  :organizations, :state
  end
end

1b. Add state_metadata to organization_details

Transition context (who triggered deletion, when) belongs in organization_details, which is the natural home for extended org metadata — parallel to namespace_details storing state_metadata for namespaces.

New file: db/migrate/<ts>_add_state_metadata_to_organization_details.rb

class AddStateMetadataToOrganizationDetails < Gitlab::Database::Migration[2.3]
  def change
    add_column :organization_details, :state_metadata, :jsonb, null: false, default: {}
  end
end

Both changes are additive with non-null defaults — no backfill needed.

1c. (Optional) deletion_scheduled_at index for background job queries

The future hard-delete worker needs to efficiently query orgs past their adjourned period. A functional index on the JSONB field in organization_details supports that:

add_index :organization_details,
  "(state_metadata->>'deletion_scheduled_at')",
  name: 'idx_org_details_deletion_scheduled_at',
  using: :btree

This can be added as a post_migrate alongside the background worker (future scope).


Phase 2: Organizations::Stateful Concern

New directory: app/models/concerns/organizations/stateful/

Model the concern parallel to Namespaces::Stateful, but stripped of concepts that don't apply to organizations (ancestor inheritance, archival, transfers, state preservation).

2a. Main concern

New file: app/models/concerns/organizations/stateful.rb

# frozen_string_literal: true

module Organizations
  module Stateful
    extend ActiveSupport::Concern

    included do
      include TransitionContext
      include TransitionCallbacks
      include TransitionLogging

      attribute :state, :integer, limit: 2, default: 0

      enum :state, {
        active: 0,
        deletion_scheduled: 1,
        deletion_in_progress: 2
      }, instance_methods: false

      state_machine :state, initial: :active do
        before_transition :update_state_metadata
        before_transition on: :schedule_deletion, do: :ensure_transition_user
        before_transition on: :schedule_deletion, do: :set_deletion_schedule_data
        before_transition on: :cancel_deletion,   do: :clear_deletion_schedule_data

        event :schedule_deletion do
          transition active: :deletion_scheduled
        end

        event :start_deletion do
          transition %i[active deletion_scheduled] => :deletion_in_progress
        end

        event :cancel_deletion do
          transition %i[deletion_scheduled deletion_in_progress] => :active
        end

        event :reschedule_deletion do
          transition deletion_in_progress: :deletion_scheduled
        end

        after_transition :log_transition
        after_failure    :update_state_metadata_on_failure
        after_failure    :log_transition_failure
      end
    end
  end
end

2b. Sub-modules

New file: app/models/concerns/organizations/stateful/transition_context.rb

module Organizations
  module Stateful
    module TransitionContext
      private

      def transition_args(transition) = transition.args.first || {}
      def transition_user(transition) = transition_args(transition)[:transition_user]
    end
  end
end

New file: app/models/concerns/organizations/stateful/transition_callbacks.rb

All metadata is written to organization_detail.state_metadata, parallel to how Namespaces::Stateful writes to namespace_details.state_metadata.

module Organizations
  module Stateful
    module TransitionCallbacks
      private

      def update_state_metadata(transition, error: nil)
        organization_detail.state_metadata.merge!(
          last_updated_at: Time.current.as_json,
          last_error: error,
          last_changed_by_user_id: transition_user(transition)&.id
        )
      end

      def set_deletion_schedule_data(transition)
        organization_detail.state_metadata.merge!(
          deletion_scheduled_at: Time.current.as_json,
          deletion_scheduled_by_user_id: transition_user(transition).id
        )
      end

      def clear_deletion_schedule_data(_transition)
        organization_detail.state_metadata.except!(
          'deletion_scheduled_at',
          'deletion_scheduled_by_user_id'
        )
      end

      def update_state_metadata_on_failure(transition)
        error_msg = "Cannot transition from #{transition.from_name} to #{transition.to_name}"
        update_state_metadata(transition, error: error_msg)
        organization_detail.save!
      end
    end
  end
end

New file: app/models/concerns/organizations/stateful/transition_logging.rb

module Organizations
  module Stateful
    module TransitionLogging
      private

      def log_transition(transition)
        Gitlab::AppLogger.info(
          message: 'Organization state transition',
          organization_id: id,
          from: transition.from_name,
          to: transition.to_name,
          event: transition.event
        )
      end

      def log_transition_failure(transition)
        Gitlab::AppLogger.error(
          message: 'Organization state transition failed',
          organization_id: id,
          from: transition.from_name,
          to: transition.to_name,
          event: transition.event,
          errors: errors.full_messages
        )
      end
    end
  end
end

Phase 3: Model Changes

OrganizationDetail model

File: app/models/organizations/organization_detail.rb

Add store_accessor for the state_metadata JSONB column so callers can read typed sub-keys without raw hash access, and add a delegation so the parent Organization can expose deletion_scheduled_at directly.

store_accessor :state_metadata, :deletion_scheduled_at, :deletion_scheduled_by_user_id

Organization model

File: app/models/organizations/organization.rb

Include the new concern

include Organizations::Stateful

Scopes (automatically generated by enum, but explicitly document key ones)

# These come from the enum definition in Organizations::Stateful:
# scope :active           — where(state: 0)
# scope :deletion_scheduled — where(state: 1)
# scope :deletion_in_progress — where(state: 2)

Predicate: emptiness check

def empty?
  groups.none? && projects.none?
end

empty? is intentionally extensible — as org-level entities (Artifact Registry, etc.) are introduced, they add themselves to this check or the service validates them separately.


Phase 4: Service — Organizations::DeleteService

New file: app/services/organizations/delete_service.rb

# frozen_string_literal: true

module Organizations
  class DeleteService < BaseService
    def initialize(organization, current_user:)
      @organization = organization
      @current_user = current_user
    end

    def execute
      return error(_('Insufficient permissions')) unless authorized?
      return error(_('Cannot delete the default organization')) if organization.default?
      return error(_('Organization must be empty before it can be deleted')) unless organization.empty?

      unless organization.schedule_deletion!(transition_user: current_user)
        return error(organization.errors.full_messages.join(', '))
      end

      # TODO: emit audit event (Phase 8)

      ServiceResponse.success(payload: { organization: organization })
    end

    private

    attr_reader :organization, :current_user

    def authorized?
      Ability.allowed?(current_user, :delete_organization, organization)
    end

    def error(message)
      ServiceResponse.error(message: message, payload: { organization: nil })
    end
  end
end

Future: Organizations::DestroyService + OrganizationDestroyWorker

For eventual hard-deletion (out of scope for this issue), follow the same pattern as Groups::DestroyService + GroupDestroyWorker: an idempotent worker that calls start_deletion!, destroys all resources, then hard-deletes the record.


Phase 5: Policy Changes

File: app/policies/organizations/organization_policy.rb

desc "Organization is the default"
condition(:default_organization, scope: :subject, score: 0) { @subject.default? }

rule { (admin | organization_owner) & ~default_organization }.enable :delete_organization

Phase 6: Finder Changes

File: app/finders/organizations/organizations_finder.rb

Soft-deleted organizations (state deletion_scheduled or deletion_in_progress) must be invisible to non-admins. Add a by_active_state filter step.

def filter_organizations(organizations)
  organizations
    .then { |o| by_user_access(o) }
    .then { |o| by_active_state(o) }
    .then { |o| by_search(o) }
end

private

def by_active_state(organizations)
  # Admins can opt in to see deleted orgs via params[:include_deleted]
  return organizations if current_user&.can_admin_all_resources? && params[:include_deleted]

  organizations.active
end

Phase 7: GraphQL

7a. New Mutation — OrganizationDelete

New file: app/graphql/mutations/organizations/delete.rb

# frozen_string_literal: true

module Mutations
  module Organizations
    class Delete < Base
      graphql_name 'OrganizationDelete'

      authorize :delete_organization

      argument :id,
        Types::GlobalIDType[::Organizations::Organization],
        required: true,
        description: 'ID of the organization to soft-delete.'

      field :organization,
        Types::Organizations::OrganizationType,
        null: true,
        description: 'The soft-deleted organization.'

      def resolve(id:)
        organization = authorized_find!(id: id)

        result = ::Organizations::DeleteService.new(
          organization,
          current_user: current_user
        ).execute

        { organization: result.payload[:organization], errors: result.errors }
      end
    end
  end
end

7b. Expose state on OrganizationType (admin-only)

File: app/graphql/types/organizations/organization_type.rb

field :state,
  GraphQL::Types::String,
  null: false,
  description: 'State of the organization.',
  authorize: :admin_organization

field :deletion_scheduled_at,
  Types::TimeType,
  null: true,
  description: 'Timestamp when deletion was scheduled. Visible to admins only.',
  authorize: :admin_organization

def deletion_scheduled_at
  ts = object.organization_detail.state_metadata['deletion_scheduled_at']
  Time.zone.parse(ts) if ts
end

7c. Register the mutation

File: app/graphql/types/mutation_type.rb

mount_mutation Mutations::Organizations::Delete

7d. Feature flag

Wrap behind delete_organization flag for safe rollout.


Phase 8: Tests

Model spec (spec/models/organizations/organization_spec.rb)

  • active scope excludes deletion_scheduled / deletion_in_progress orgs
  • deletion_scheduled scope returns only state=1 orgs
  • schedule_deletion! transitions activedeletion_scheduled, populates state_metadata
  • cancel_deletion! transitions deletion_scheduledactive, clears deletion metadata
  • empty? returns false when org has groups; false when org has projects; true when empty
  • Cannot schedule_deletion! from deletion_scheduled state (invalid transition)

Concern spec (spec/models/concerns/organizations/stateful_spec.rb)

  • All valid transitions succeed
  • Invalid transitions fail and set state error
  • set_deletion_schedule_data stores deletion_scheduled_at and deletion_scheduled_by_user_id
  • clear_deletion_schedule_data removes those keys
  • ensure_transition_user prevents schedule_deletion without a transition_user
  • update_state_metadata_on_failure logs error into state_metadata

Service spec (spec/services/organizations/delete_service_spec.rb)

  • Returns error for unauthorized user
  • Returns error for default organization
  • Returns error when org has groups or projects
  • Returns error when schedule_deletion! fails (e.g., invalid transition)
  • Transitions org to deletion_scheduled on success
  • Returns success ServiceResponse with organization

Policy spec (spec/policies/organizations/organization_policy_spec.rb)

  • delete_organization enabled for org owners
  • delete_organization enabled for instance admins
  • delete_organization disabled for regular members
  • delete_organization disabled for the default organization

Finder spec (spec/finders/organizations/organizations_finder_spec.rb)

  • deletion_scheduled orgs excluded for regular users
  • deletion_scheduled orgs excluded for admins by default
  • deletion_scheduled orgs included when include_deleted: true for admins

GraphQL mutation spec (spec/requests/api/graphql/mutations/organizations/delete_spec.rb)

  • Succeeds for org owner on empty org (returns org in deletion_scheduled state)
  • Succeeds for instance admin on empty org
  • Returns permission error for regular user
  • Returns error when org has groups
  • Returns error when org has projects
  • Returns error for default org
  • state field reflects deletion_scheduled after mutation

Phase 9: Audit Events

Emit an organization_deletion_scheduled audit event in DeleteService#execute:

::Gitlab::Audit::Auditor.audit({
  name: 'organization_deletion_scheduled',
  author: current_user,
  scope: organization,
  target: organization,
  message: "Scheduled organization '#{organization.name}' for deletion"
})

File Change Summary

File Type Notes
db/migrate/<ts>_add_state_to_organizations.rb New state smallint
db/migrate/<ts>_add_state_metadata_to_organization_details.rb New state_metadata jsonb
app/models/concerns/organizations/stateful.rb New State machine concern
app/models/concerns/organizations/stateful/transition_context.rb New
app/models/concerns/organizations/stateful/transition_callbacks.rb New
app/models/concerns/organizations/stateful/transition_logging.rb New
app/models/organizations/organization.rb Modify Include Organizations::Stateful, add empty?
app/services/organizations/delete_service.rb New
app/policies/organizations/organization_policy.rb Modify Add delete_organization ability
app/finders/organizations/organizations_finder.rb Modify Add by_active_state filter
app/graphql/mutations/organizations/delete.rb New
app/graphql/types/organizations/organization_type.rb Modify Expose state, deletion_scheduled_at
app/graphql/types/mutation_type.rb Modify Mount mutation
spec/models/concerns/organizations/stateful_spec.rb New
spec/models/organizations/organization_spec.rb Modify
spec/services/organizations/delete_service_spec.rb New
spec/policies/organizations/organization_policy_spec.rb Modify
spec/finders/organizations/organizations_finder_spec.rb Modify
spec/requests/api/graphql/mutations/organizations/delete_spec.rb New

Differences from Namespaces::Stateful

Aspect Namespaces::Stateful Organizations::Stateful
Ancestor hierarchy Yes — ancestor_inherited state, validate_ancestors_state No — orgs have no parent
Archival Yes — archived state, archive/unarchive events No (not in scope)
State preservation Yes — StatePreservation module, state_metadata['preserved_states'] No — cancel always returns to active
Transfer Yes — transfer_in_progress, start/complete/cancel_transfer No
Metadata storage namespace_details.state_metadata (jsonb, separate table) organization_details.state_metadata (jsonb, same separate-table pattern)
Deletion metadata deletion_scheduled_at, deletion_scheduled_by_user_id in metadata Same pattern

Open Questions / Decisions Needed

  1. empty? extensibility: Should this be a configurable registry (so future org-level entities self-register) or kept as explicit method calls? For now, explicit is simpler.

  2. Adjourned period: Should soft-deleted orgs be automatically hard-deleted after N days (like groups use deletion_adjourned_period)? Not in scope but should be a follow-up issue.

  3. include_deleted as GraphQL argument: Expose includeDeleted: Boolean on the organizations query (admin-only)? Useful for admin tooling. Propose as follow-up.

  4. REST API: DELETE /api/v4/organizations/:id for API parity. Out of scope.

  5. OrganizationRestore mutation: Issue says no UI required, but an admin-only mutation wrapping cancel_deletion! would be useful. Propose as follow-up.

tag:gitlab.com,2026-03-19:5223010021 Rémy Coutable commented on merge request !228030 at GitLab.org / GitLab 2026-03-19T16:18:23Z rymai Rémy Coutable

@alexpooley @smaglangit With the help of Claude Code, I tried to implement what you both suggested at #572153 (comment 2800085821), but I'm not sure if we really want to prevent anonymous users from reading organizations? I'm crossposting gitlab-com/content-sites/handbook!18951 (diffs, comment 3169101390) since we are also discussing it there. Let me know what you think!

tag:gitlab.com,2026-03-19:5223000386 Rémy Coutable commented on issue #572153 at GitLab.org / GitLab 2026-03-19T16:15:56Z rymai Rémy Coutable

@mandrewsgl @smaglangit FYI I used this issue to learn how I could leverage Claude Code as much as possible, you can see the end-result at !228030.

tag:gitlab.com,2026-03-19:5222988761 Rémy Coutable commented on merge request !18951 at GitLab.com / Content Sites / handbook 2026-03-19T16:13:32Z rymai Rémy Coutable
  - Public Organizations can be seen by logged-in users, but not anonymous users. They can contain public and private Groups and Projects.
tag:gitlab.com,2026-03-19:5222981438 Rémy Coutable commented on merge request !18951 at GitLab.com / Content Sites / handbook 2026-03-19T16:11:53Z rymai Rémy Coutable

Good timing, as I worked on gitlab-org/gitlab#572153 => gitlab-org/gitlab!228030 where it was advised that anonymous users cannot see any organizations.

That means we should probably update the table to replace "Everyone" with "Logged-in users".

tag:gitlab.com,2026-03-19:5222784740 Rémy Coutable commented on merge request !228030 at GitLab.org / GitLab 2026-03-19T15:30:55Z rymai Rémy Coutable

Good suggestion — switched to Arel in the latest push. The condition is now built as:

org_users_table = ::Organizations::OrganizationUser.arel_table
orgs_table = ::Organizations::Organization.arel_table
member_condition = org_users_table[:user_id].eq(current_user.id)
public_condition = orgs_table[:visibility_level].eq(::Organizations::Organization::PUBLIC)

organizations
  .left_outer_joins(:organization_users)
  .where(member_condition.or(public_condition))
  .distinct
tag:gitlab.com,2026-03-19:5222784291 Rémy Coutable commented on merge request !228030 at GitLab.org / GitLab 2026-03-19T15:30:50Z rymai Rémy Coutable

Good point on consistency — changed to organizations.none in the latest push.

tag:gitlab.com,2026-03-19:5222762132 Rémy Coutable pushed to project branch fix/move-organization-authorization-to-finder at GitLab.org / GitLab 2026-03-19T15:26:54Z rymai Rémy Coutable

Rémy Coutable (d6f08476) at 19 Mar 15:26

Move organization authorization from GraphQL types to finder

tag:gitlab.com,2026-03-19:5222745243 Rémy Coutable commented on merge request !2777 at GitLab.org / cli 2026-03-19T15:23:30Z rymai Rémy Coutable

@timofurrer The MR seems idle so you might want to take over if this is of interest, or close it for now?

tag:gitlab.com,2026-03-19:5222741002 Rémy Coutable commented on merge request !224382 at GitLab.org / GitLab 2026-03-19T15:22:38Z rymai Rémy Coutable

@hfyngvason I think this requires your input.

tag:gitlab.com,2026-03-19:5222723823 Rémy Coutable commented on merge request !1443 at GitLab.org / Frontend / renovate-gitlab-bot 2026-03-19T15:18:58Z rymai Rémy Coutable

@grantyoung Looks good to me, thanks! ❤️ 💛 💚 💜

tag:gitlab.com,2026-03-19:5222723788 Rémy Coutable approved merge request !1443: Enable automated Python requirements updates for Gitlab Environment Toolkit at GitLab.org / Frontend / renovate-g... 2026-03-19T15:18:57Z rymai Rémy Coutable

Enables monthly automated updates for Ansible Python dependencies requirements.txt in the Gitlab Environment Toolkit project.

tag:gitlab.com,2026-03-19:5222671261 Rémy Coutable commented on merge request !228030 at GitLab.org / GitLab 2026-03-19T15:08:07Z rymai Rémy Coutable

Agreed — the Danger bot flagged this too. I'll add the ~database and ~"database::review pending" labels and assign the suggested DB reviewer (@eugielimpin) so the query plan and index coverage get a proper look.

tag:gitlab.com,2026-03-19:5222666250 Rémy Coutable commented on merge request !228030 at GitLab.org / GitLab 2026-03-19T15:07:07Z rymai Rémy Coutable

Good call — I audited all resolvers and mutations that return Types::Organizations::OrganizationType:

  • Resolvers::Organizations::OrganizationResolver (singular lookup): uses authorized_find! with authorize :read_organization
  • Resolvers::Organizations::OrganizationsResolver (this MR): authorization delegated to OrganizationsFinder#by_user_access
  • Resolvers::Users::OrganizationsResolver: uses authorizes_object! + authorize :read_user_organizations + user-scoped UserOrganizationsFinder
  • Mutations::Organizations::Base: returns the mutated object after a write operation; authorization is enforced by the mutation itself ✓

All existing entry points are accounted for. The rubocop disable is safe as-is, and the comment on the disable explains the rationale so future contributors know what to look for.