Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion readthedocs/organizations/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class OrganizationForm(SimpleHistoryModelForm):

# We use the organization slug + project name
# to form the final project slug.
# A valid project slug is 63 chars long.
# A valid project slug is 55 chars long.
name = forms.CharField(max_length=32)

class Meta:
Expand Down
8 changes: 8 additions & 0 deletions readthedocs/projects/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ def clean_name(self):
raise forms.ValidationError(
_("Invalid project name"),
)
if len(potential_slug) > 55:
raise forms.ValidationError(
_(
"Project name is too long, the generated slug must be 55 characters or less. "
"The current slug would be: %(slug)s"
),
params={"slug": potential_slug},
)
Copy link
Member

@ericholscher ericholscher Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I'm not sure we want to error here, but just truncate. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://gh.io/copilot-coding-agent-docs I'm not sure we want to error here, but just truncate. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I'm not sure we want to error here, but just truncate. What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to truncate instead of error. Slugs longer than 55 characters are now automatically truncated in both the form validation and model save method. This applies only to new projects. (commit: git rev-parse --short HEAD)


return name

Expand Down
65 changes: 65 additions & 0 deletions readthedocs/projects/migrations/0156_reduce_slug_max_length.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Generated by Django 5.2.4 on 2025-01-XX XX:XX
from django.db import migrations
from django.db import models
from django.db.models.functions import Length
from django_safemigrate import Safe


def forwards_func(apps, schema_editor):
"""
Truncate slugs and names longer than 55 characters.

PR builds use the format {project.slug}--{pr.number}, so we need to leave
room for the suffix. With max 6 digits for PR numbers + 2 for '--', we need
to reduce the max from 63 to 55 characters.
"""
max_length = 55
Project = apps.get_model("projects", "Project")

# Truncate slugs that are too long
projects_invalid_slug = Project.objects.annotate(slug_length=Length("slug")).filter(
slug_length__gt=max_length
)
for project in projects_invalid_slug:
project.slug = project.slug[:max_length]
project.save()

# Truncate names that are too long
projects_invalid_name = Project.objects.annotate(name_length=Length("name")).filter(
name_length__gt=max_length
)
for project in projects_invalid_name:
project.name = project.name[:max_length]
project.save()


class Migration(migrations.Migration):
safe = Safe.after_deploy()

dependencies = [
("projects", "0155_custom_git_checkout_step"),
]

operations = [
migrations.RunPython(forwards_func),
migrations.AlterField(
model_name="project",
name="slug",
field=models.SlugField(max_length=55, unique=True, verbose_name="Slug"),
),
migrations.AlterField(
model_name="project",
name="name",
field=models.CharField(max_length=55, verbose_name="Name"),
),
migrations.AlterField(
model_name="historicalproject",
name="slug",
field=models.SlugField(max_length=55, verbose_name="Slug", db_index=True),
),
migrations.AlterField(
model_name="historicalproject",
name="name",
field=models.CharField(max_length=55, verbose_name="Name"),
),
]
5 changes: 3 additions & 2 deletions readthedocs/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,9 @@ class Project(models.Model):
related_name="projects",
)
# A DNS label can contain up to 63 characters.
name = models.CharField(_("Name"), max_length=63)
slug = models.SlugField(_("Slug"), max_length=63, unique=True)
# We limit to 55 to account for PR build suffixes (--{pr_number}).
name = models.CharField(_("Name"), max_length=55)
slug = models.SlugField(_("Slug"), max_length=55, unique=True)
description = models.TextField(
_("Description"),
blank=True,
Expand Down
28 changes: 28 additions & 0 deletions readthedocs/rtd_tests/tests/test_project_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,34 @@ def test_empty_slug(self):
self.assertFalse(form.is_valid())
self.assertIn("name", form.errors)

def test_slug_too_long(self):
"""Test that project names that generate slugs longer than 55 chars are rejected."""
# Test with a name that generates a 56-character slug
long_name = "a" * 56
initial = {
"name": long_name,
"repo_type": "git",
"repo": "https://github.com/user/repository",
"language": "en",
}
form = ProjectBasicsForm(initial)
self.assertFalse(form.is_valid())
self.assertIn("name", form.errors)
self.assertIn("slug must be 55 characters or less", str(form.errors["name"]))

def test_slug_max_length(self):
"""Test that project names that generate exactly 55-character slugs are accepted."""
# Test with a name that generates exactly 55-character slug
max_name = "a" * 55
initial = {
"name": max_name,
"repo_type": "git",
"repo": "https://github.com/user/repository",
"language": "en",
}
form = ProjectBasicsForm(initial)
self.assertTrue(form.is_valid())

@override_settings(ALLOW_PRIVATE_REPOS=False)
def test_length_of_tags(self):
project = get(Project)
Expand Down