Bulk Ownership Change of First Posts by Tag – Script Review and Suggestions

Hello,

I am looking to change the ownership of all opening posts in event topics that are tagged with “ethan” such that the owner changes from “system” to the user “Ethan_Hoom1”. I am running a self-hosted instance and have Rails console access.

After reviewing the recommendations at Administrative Bulk Operations and Change ownership of all posts by a specific user, I have prepared the following script. I would appreciate any suggestions or best practices before running it in production:


# Find the tag object with the name "ethan"
tag = Tag.find_by(name: "ethan")

# Stop immediately if the tag does not exist
raise "Tag not found" unless tag


# Find the user who should become the new owner of the posts
# username_lower is safer than username (case-insensitive)
new_owner = User.find_by(username_lower: "ethan_hoom1")

# Stop immediately if the target user does not exist
raise "New owner user not found" unless new_owner


# Find the system user account
# Fallback to Discourse.system_user in case the record lookup fails
system_user = User.find_by(username_lower: "system") || Discourse.system_user

# Stop immediately if the system user cannot be found
raise "System user not found" unless system_user


# When true, the script will only print what it WOULD change
# and will not modify anything in the database
DRY_RUN = true

# Optional safety limit for first run (e.g. 10 or 50)
# Set to nil to process all matching posts
LIMIT = nil


# Build an ActiveRecord query for the posts we want to modify
scope = Post
  # Join posts → topics → topic_tags so we can filter by tag
  .joins(topic: :topic_tags)

  # Only target the opening post in each topic
  .where(post_number: 1)

  # Only target posts currently owned by the system user
  .where(user_id: system_user.id)

  # Only include topics that have the "ethan" tag
  .where(topic_tags: { tag_id: tag.id })

  # Exclude private messages and deleted topics
  .where(topics: {
    archetype: Archetype.default,
    deleted_at: nil
  })


# If LIMIT is set, restrict how many posts are processed
scope = scope.limit(LIMIT) if LIMIT


# Iterate through matching posts in batches to avoid memory issues
scope.find_each(batch_size: 100) do |first_post|

  # Store the topic ID for logging output
  topic_id = first_post.topic_id


  # If dry-run mode is enabled, do not modify anything
  if DRY_RUN
    puts "[DRY RUN] Would change owner for topic #{topic_id} (post #{first_post.id})"
    next
  end


  begin
    # Use Discourse's official ownership-changing service
    PostOwnerChanger.new(
      # Only change ownership of the opening post
      post_ids: [first_post.id],

      # The topic containing the post
      topic_id: topic_id,

      # The user who should become the new owner
      new_owner: new_owner,

      # The user performing the action (system user)
      acting_user: system_user,

      # Do not create an edit revision for this change
      skip_revision: true
    ).change_owner!


    # Log success to the console
    puts "Changed owner for topic #{topic_id} (post #{first_post.id})"

  rescue => e
    # If anything fails, log the error but continue processing others
    puts "FAILED topic #{topic_id} (post #{first_post.id}): #{e.class}: #{e.message}"
  end
end

Questions:

  • Are there any caveats or edge cases I should be aware of with PostOwnerChanger for this use case?
  • Is it advisable to also update the topic.user field as shown in the optional line?
  • Do you have any recommendations for further safety or performance improvements regarding this batch process?

Thank you for your help and for all the great documentation!

1 Like