Ignore description, description_html, cached_markdown_version columns from projects

What does this MR do and why?

This MR moves description, description_html, and cached_markdown_version read/write from the projects table to namespace_details (via the project's project_namespace), then adds ignore_columns so Rails stops selecting those columns from projects.

It is the projects counterpart to what was done for namespaces in Ignore description related columns from namespa... (!188090 - merged) • Abdul Wadood • 18.1.

The BackfillProjectNamespaceDetails batched background migration is already finalized (finalized_by: 20260120232929), so all data is already in namespace_details.

Reading and writing

  • A description can be assigned before project_namespace exists (it is built in a before_validation), and project_namespace may be rebuilt afterwards, so the value is buffered on a detached Namespace::Detail and copied across in an after_save. It has to be after_save, because Namespace creates its own namespace_details in an after_create that would otherwise clobber an earlier assignment.
  • The description length validation stays on Project as well as Namespace::Detail, so an over-long description still fails validation on :description instead of raising from the after_save.
  • Project#after_import refreshes the markdown cache on namespace_details, not on the project: refreshing on the project writes cached_markdown_version unconditionally, and that column is now ignored.
  • Namespace::Detail exposes project and group, which CacheMarkdownField#banzai_render_context needs so that references inside a description still render as links. GroupChildEntity relies on this when rendering a project child, which otherwise 500s the group children endpoint.

Preloading

Since description now lives on namespace_details, every path that renders a project description has to preload project_namespace: :namespace_details or it costs two extra queries per row. The with_project_namespace_details scope wraps that preload and is chained everywhere a project description is rendered:

  • Project.with_api_entity_associations
  • Project.with_web_entity_associations
  • Project.with_group_child_entity_associations
  • the GraphQL look-ahead preloads
  • Explore::ProjectsController (the HTML list and the Atom feed)
  • Projects::ForksController
  • Admin::UsersController
  • Project.search and sorted_by_similarity_desc no longer match or rank on description, only on path and name.
  • Joining namespace_details was tried and rejected: it makes these scopes structurally incompatible with the or used in the archived scopes, raising ArgumentError and a 500 from the projects GraphQL query.
  • namespace_details.description has no index, unlike projects.description, which had a gin trigram index that goes away with the column.
  • This aligns Project.search and Project.sorted_by_similarity_desc with Namespace.search and Group.sorted_by_similarity_desc, which already dropped description matching; the now-unused exclude_description: keyword argument is removed from the two Project methods.
  • User-facing behaviour change: GET /projects?search= and project search in the UI no longer match on descriptions. Worth a follow-up issue if that capability is needed back (would require a gin trigram index on namespace_details.description).

Audit events

  • Project description changes are now audited by Namespaces::NamespaceDetailChangesAuditor, the class that already audits group descriptions, generalized to accept a group or a project container.
  • The project_description_updated event name does not change, and its scope and target_details are still the project. What changes is the event's target, now the Namespace::Detail record instead of the Project, the same shape the group event already has.
  • project_description_updated is saved_to_database: false, streamed: true, so this payload change only reaches streaming consumers; no rows in audit_events are affected. This is deliberate, per the request in the issue that introduced the event: #377769 (closed)

Closes #590461

References

How to set up and validate locally

In the rails console:

  1. project = Project.first; project.description reads from namespace_details.
  2. project.update!(description: 'new desc') persists to namespace_details.
  3. project.namespace_details.description returns 'new desc'.
  4. Project.create!(namespace: Namespace.first, name: 'x', path: 'x', description: 'set at creation', creator: User.first).reload.description returns 'set at creation'.
  5. Project.search('new desc') returns the project.
  6. With an external streaming destination configured, updating a project description streams a project_description_updated event whose target is the project's namespace details record.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

Edited by Rémy Coutable

Merge request reports

Loading
Loading