Skip to content

Commit da2abd5

Browse files
committed
Merge tag '15.2.0' into rel
2 parents 75d90b6 + d9f56e0 commit da2abd5

File tree

25 files changed

+580
-494
lines changed

25 files changed

+580
-494
lines changed

docs/conf.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@
6464
ogp_use_first_image = True # https://github.com/readthedocs/blog/pull/118
6565
ogp_image = "https://docs.readthedocs.io/en/latest/_static/img/logo-opengraph.png"
6666
# Inspired by https://github.com/executablebooks/MyST-Parser/pull/404/
67-
ogp_custom_meta_tags = [
67+
ogp_custom_meta_tags = (
6868
'<meta name="twitter:card" content="summary_large_image" />',
69-
]
69+
)
7070
ogp_enable_meta_description = True
7171
ogp_description_length = 300
7272

@@ -81,7 +81,7 @@
8181

8282
master_doc = "index"
8383
copyright = "Read the Docs, Inc & contributors"
84-
version = "15.1.0"
84+
version = "15.2.0"
8585
release = version
8686
exclude_patterns = ["_build", "shared", "_includes"]
8787
default_role = "obj"

docs/user/guides/conda.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,9 @@ with these contents:
119119
version: 2
120120
121121
build:
122-
os: "ubuntu-20.04"
122+
os: "ubuntu-24.04"
123123
tools:
124-
python: "mambaforge-22.9"
124+
python: "miniconda3-3.12-24.9"
125125
126126
conda:
127127
environment: environment.yml

readthedocs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Read the Docs."""
22

3-
__version__ = "15.1.0"
3+
__version__ = "15.2.0"

readthedocs/api/v2/models.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@ def create_key(self, project):
1616
"""
1717
Create a new API key for a project.
1818
19-
Build API keys are valid for 3 hours,
19+
Build API keys are valid for
20+
21+
- project or default build time limit
22+
- plus 25% to cleanup task once build is finished
23+
- plus extra time to allow multiple retries (concurrency limit reached)
24+
2025
and can be revoked at any time by hitting the /api/v2/revoke/ endpoint.
2126
"""
22-
# Use the project or default build time limit + 25% for the API token
23-
delta = (project.container_time_limit or settings.BUILD_TIME_LIMIT) * 1.25
27+
delta = (
28+
project.container_time_limit or settings.BUILD_TIME_LIMIT
29+
) * 1.25 + settings.RTD_BUILDS_RETRY_DELAY * settings.RTD_BUILDS_MAX_RETRIES
2430
expiry_date = timezone.now() + timedelta(seconds=delta)
2531
name_max_length = self.model._meta.get_field("name").max_length
2632
return super().create_key(

readthedocs/api/v2/views/model_views.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ def concurrent(self, request, **kwargs):
305305
# We make this endpoint public because we don't want to expose the build API key inside the user's container.
306306
# To emulate "auth" we check for the builder hostname to match the `Build.builder` defined in the database.
307307
permission_classes=[],
308+
# We can't use the default `get_queryset()` method because it's filtered by build API key and/or user access.
309+
# Since we don't want to check for permissions here we need to use a custom queryset here.
310+
get_queryset=lambda: Build.objects.all(),
308311
methods=["post"],
309312
)
310313
def healthcheck(self, request, **kwargs):

readthedocs/oauth/services/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ class UserService(Service):
144144
def __init__(self, user, account):
145145
self.user = user
146146
self.account = account
147+
# Cache organizations to avoid multiple DB hits
148+
# when syncing repositories that belong to the same organization.
149+
# Used by `create_organization` method in subclasses.
150+
self._organizations_cache = {}
147151
structlog.contextvars.bind_contextvars(
148152
user_username=self.user.username,
149153
social_provider=self.allauth_provider.id,

readthedocs/oauth/services/bitbucket.py

Lines changed: 65 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -63,24 +63,26 @@ def sync_repositories(self):
6363
"https://bitbucket.org/api/2.0/repositories/",
6464
role="admin",
6565
)
66-
admin_repo_relations = RemoteRepositoryRelation.objects.filter(
66+
RemoteRepositoryRelation.objects.filter(
6767
user=self.user,
6868
account=self.account,
6969
remote_repository__vcs_provider=self.vcs_provider_slug,
7070
remote_repository__remote_id__in=[r["uuid"] for r in resp],
71-
)
72-
for remote_repository_relation in admin_repo_relations:
73-
remote_repository_relation.admin = True
74-
remote_repository_relation.save()
71+
).update(admin=True)
7572
except (TypeError, ValueError):
76-
pass
73+
log.warning("Error syncing Bitbucket admin repositories")
7774

7875
return remote_ids
7976

8077
def sync_organizations(self):
81-
"""Sync Bitbucket workspaces(our RemoteOrganization) and workspace repositories."""
78+
"""
79+
Sync Bitbucket workspaces (organizations).
80+
81+
This method only creates the relationships between the
82+
organizations and the user, as all the repositories
83+
are already created in the sync_repositories method.
84+
"""
8285
organization_remote_ids = []
83-
repository_remote_ids = []
8486

8587
try:
8688
workspaces = self.paginate(
@@ -89,18 +91,8 @@ def sync_organizations(self):
8991
)
9092
for workspace in workspaces:
9193
remote_organization = self.create_organization(workspace)
92-
repos = self.paginate(workspace["links"]["repositories"]["href"])
93-
94+
remote_organization.get_remote_organization_relation(self.user, self.account)
9495
organization_remote_ids.append(remote_organization.remote_id)
95-
96-
for repo in repos:
97-
remote_repository = self.create_repository(
98-
repo,
99-
organization=remote_organization,
100-
)
101-
if remote_repository:
102-
repository_remote_ids.append(remote_repository.remote_id)
103-
10496
except ValueError:
10597
log.warning("Error syncing Bitbucket organizations")
10698
raise SyncServiceError(
@@ -109,9 +101,9 @@ def sync_organizations(self):
109101
)
110102
)
111103

112-
return organization_remote_ids, repository_remote_ids
104+
return organization_remote_ids, []
113105

114-
def create_repository(self, fields, privacy=None, organization=None):
106+
def create_repository(self, fields, privacy=None):
115107
"""
116108
Update or create a repository from Bitbucket API response.
117109
@@ -136,42 +128,15 @@ def create_repository(self, fields, privacy=None, organization=None):
136128
repo, _ = RemoteRepository.objects.get_or_create(
137129
remote_id=fields["uuid"], vcs_provider=self.vcs_provider_slug
138130
)
139-
repo.get_remote_repository_relation(self.user, self.account)
131+
self._update_repository_from_fields(repo, fields)
140132

141-
if repo.organization and repo.organization != organization:
142-
log.debug(
143-
"Not importing repository because mismatched orgs.",
144-
repository=fields["name"],
145-
)
146-
return None
147-
148-
repo.organization = organization
149-
repo.name = fields["name"]
150-
repo.full_name = fields["full_name"]
151-
repo.description = fields["description"]
152-
repo.private = fields["is_private"]
153-
154-
# Default to HTTPS, use SSH for private repositories
155-
clone_urls = {u["name"]: u["href"] for u in fields["links"]["clone"]}
156-
repo.clone_url = self.https_url_pattern.sub(
157-
"https://bitbucket.org/",
158-
clone_urls.get("https"),
133+
# The repositories API doesn't return the admin status of the user,
134+
# so we default to False, and then update it later using another API call.
135+
remote_repository_relation = repo.get_remote_repository_relation(
136+
self.user, self.account
159137
)
160-
repo.ssh_url = clone_urls.get("ssh")
161-
if repo.private:
162-
repo.clone_url = repo.ssh_url
163-
164-
repo.html_url = fields["links"]["html"]["href"]
165-
repo.vcs = fields["scm"]
166-
mainbranch = fields.get("mainbranch") or {}
167-
repo.default_branch = mainbranch.get("name")
168-
169-
avatar_url = fields["links"]["avatar"]["href"] or ""
170-
repo.avatar_url = re.sub(r"\/16\/$", r"/32/", avatar_url)
171-
if not repo.avatar_url:
172-
repo.avatar_url = self.default_user_avatar_url
173-
174-
repo.save()
138+
remote_repository_relation.admin = False
139+
remote_repository_relation.save()
175140

176141
return repo
177142

@@ -180,17 +145,59 @@ def create_repository(self, fields, privacy=None, organization=None):
180145
repository=fields["name"],
181146
)
182147

148+
def _update_repository_from_fields(self, repo, fields):
149+
# All repositories are created under a workspace,
150+
# which we consider an organization.
151+
organization = self.create_organization(fields["workspace"])
152+
repo.organization = organization
153+
repo.name = fields["name"]
154+
repo.full_name = fields["full_name"]
155+
repo.description = fields["description"]
156+
repo.private = fields["is_private"]
157+
158+
# Default to HTTPS, use SSH for private repositories
159+
clone_urls = {u["name"]: u["href"] for u in fields["links"]["clone"]}
160+
repo.clone_url = self.https_url_pattern.sub(
161+
"https://bitbucket.org/",
162+
clone_urls.get("https"),
163+
)
164+
repo.ssh_url = clone_urls.get("ssh")
165+
if repo.private:
166+
repo.clone_url = repo.ssh_url
167+
168+
repo.html_url = fields["links"]["html"]["href"]
169+
repo.vcs = fields["scm"]
170+
mainbranch = fields.get("mainbranch") or {}
171+
repo.default_branch = mainbranch.get("name")
172+
173+
avatar_url = fields["links"]["avatar"]["href"] or ""
174+
repo.avatar_url = re.sub(r"\/16\/$", r"/32/", avatar_url)
175+
if not repo.avatar_url:
176+
repo.avatar_url = self.default_user_avatar_url
177+
178+
repo.save()
179+
183180
def create_organization(self, fields):
184181
"""
185182
Update or create remote organization from Bitbucket API response.
186183
187184
:param fields: dictionary response of data from API
188185
:rtype: RemoteOrganization
186+
187+
.. note::
188+
189+
This method caches organizations by their remote ID to avoid
190+
unnecessary database queries, specially when creating
191+
multiple repositories that belong to the same organization.
189192
"""
193+
organization_id = fields["uuid"]
194+
if organization_id in self._organizations_cache:
195+
return self._organizations_cache[organization_id]
196+
190197
organization, _ = RemoteOrganization.objects.get_or_create(
191-
remote_id=fields["uuid"], vcs_provider=self.vcs_provider_slug
198+
remote_id=organization_id,
199+
vcs_provider=self.vcs_provider_slug,
192200
)
193-
organization.get_remote_organization_relation(self.user, self.account)
194201

195202
organization.slug = fields.get("slug")
196203
organization.name = fields.get("name")
@@ -201,6 +208,7 @@ def create_organization(self, fields):
201208

202209
organization.save()
203210

211+
self._organizations_cache[organization_id] = organization
204212
return organization
205213

206214
def get_next_url_to_paginate(self, response):

0 commit comments

Comments
 (0)