Skip to content

Commit 6c55a61

Browse files
committed
api.views: add try push handover functionality (bug 2002566)
- add functionality that adds try_task_config.json file - update landing worker to process handover jobs - add PullRequestTryLintAPIView - add LandingJob.handover_repo field and migrations - add LandingJob.is_handed_over field and migrations - add Repo.is_try helper method
1 parent 52b5e9a commit 6c55a61

File tree

6 files changed

+133
-0
lines changed

6 files changed

+133
-0
lines changed

src/lando/api/legacy/workers/landing_worker.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import configparser
2+
import json
23
import logging
34
import subprocess
45
from pathlib import Path
@@ -168,6 +169,19 @@ def run_job(self, job: LandingJob) -> bool:
168169

169170
return True
170171

172+
def add_try_task_config(self, scm: AbstractSCM):
173+
with (Path(scm.path) / "try_task_config.json").open("x") as f:
174+
content = json.dumps(
175+
{
176+
"parameters": {
177+
"optimize_target_tasks": True,
178+
"target_tasks_method": "codereview",
179+
},
180+
"version": 2,
181+
}
182+
)
183+
f.write(content)
184+
171185
def convert_patches_to_diff(self, scm: AbstractSCM, job: LandingJob):
172186
"""Generate a unified diff from multiple patches stored in a revision."""
173187
# NOTE: this only applies to git patches that are downloaded from GitHub
@@ -215,6 +229,14 @@ def apply_patch(revision: Revision):
215229

216230
self.update_repo(repo, job, scm, job.target_commit_hash)
217231

232+
if job.is_pull_request_job and job.handover_repo and job.handover_repo.is_try:
233+
self.add_try_task_config(scm)
234+
self.convert_patches_to_diff(scm, job)
235+
job.handover()
236+
message = "Job deferred to try repo."
237+
job.transition_status(JobAction.DEFER, message=message)
238+
raise TemporaryFailureException(message)
239+
218240
if job.is_pull_request_job:
219241
self.convert_patches_to_diff(scm, job)
220242
self.update_repo(repo, job, scm, job.target_commit_hash)

src/lando/api/views.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,43 @@ def get(self, request: WSGIRequest, repo_name: str, number: int) -> JsonResponse
303303
target_repo, pull_request, request
304304
)
305305
return JsonResponse(warnings_and_blockers)
306+
307+
308+
class PullRequestTryPushAPIView(APIView):
309+
@method_decorator(require_authenticated_user)
310+
def post(
311+
self, request: WSGIRequest, repo_name: str, pull_number: int
312+
) -> JsonResponse:
313+
try:
314+
try_repo = Repo.objects.get(name="try")
315+
except Repo.DoesNotExist:
316+
return JsonResponse({"errors": ["Try repo does not exist"]}, 500)
317+
318+
target_repo = Repo.objects.get(name=repo_name)
319+
client = GitHubAPIClient(target_repo.url)
320+
ldap_username = request.user.email
321+
pull_request = client.build_pull_request(pull_number)
322+
323+
job = LandingJob.objects.create(
324+
target_repo=target_repo,
325+
is_handed_over=False,
326+
handover_repo=try_repo,
327+
requester_email=ldap_username,
328+
)
329+
author_name, author_email = pull_request.author
330+
patch_data = {
331+
"author_name": author_name,
332+
"author_email": author_email,
333+
"commit_message": pull_request.commit_message,
334+
"timestamp": int(datetime.now().timestamp()),
335+
}
336+
revision = Revision.objects.create(
337+
pull_number=pull_request.number,
338+
patches=pull_request.patch,
339+
patch_data=patch_data,
340+
)
341+
add_revisions_to_job([revision], job)
342+
job.status = JobStatus.SUBMITTED
343+
job.save()
344+
345+
return JsonResponse({"id": job.id}, status=201)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 5.2.8 on 2025-12-08 19:48
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("main", "0040_landingjob_is_pull_request_job"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="landingjob",
16+
name="handover_repo",
17+
field=models.ForeignKey(
18+
blank=True,
19+
null=True,
20+
on_delete=django.db.models.deletion.SET_NULL,
21+
related_name="handover_jobs",
22+
to="main.repo",
23+
),
24+
),
25+
migrations.AddField(
26+
model_name="landingjob",
27+
name="is_handed_over",
28+
field=models.BooleanField(blank=True, null=True),
29+
),
30+
]

src/lando/main/models/landing_job.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ class LandingJob(BaseJob):
4848

4949
is_pull_request_job = models.BooleanField(default=False, blank=True)
5050

51+
# Reference to the handover repo. A handover repo is a repo that a landing
52+
# job is handed over to after being processed in a previous state. If this value
53+
# is set, the job will be processed twice, but pushed once.
54+
handover_repo = models.ForeignKey(
55+
Repo,
56+
on_delete=models.SET_NULL,
57+
null=True,
58+
blank=True,
59+
related_name="handover_jobs",
60+
)
61+
is_handed_over = models.BooleanField(null=True, blank=True)
62+
5163
@property
5264
def landed_phabricator_revisions(self) -> dict:
5365
"""Return a mapping associating Phabricator revision IDs with the ID of the landed Diff."""
@@ -113,6 +125,25 @@ def serialized_landing_path(self) -> list[dict]:
113125
for revision_id, diff_id in self.landed_revisions.items()
114126
]
115127

128+
def handover(self):
129+
if self.is_handed_over:
130+
raise ValueError(f"{self} is already handed over")
131+
if not self.is_pull_request_job:
132+
raise NotImplementedError(
133+
"Handover is only supported for pull request jobs"
134+
)
135+
if not self.handover_repo:
136+
raise ValueError(f"{self} does not have a handover repo defined")
137+
if not self.handover_repo.is_try:
138+
raise NotImplementedError(
139+
"Handover is currently only supported for try repo"
140+
)
141+
142+
self.target_repo = self.handover_repo
143+
self.is_pull_request_job = False
144+
self.is_handed_over = True
145+
self.save()
146+
116147
@property
117148
def has_phabricator_revisions(self) -> bool:
118149
"""Indicate if this job has Phabricator revisions by checking the first in the stack."""

src/lando/main/models/repo.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ class HooksChoices(models.TextChoices):
101101
def path(self) -> str:
102102
return str(self.system_path) or self.get_system_path()
103103

104+
@property
105+
def is_try(self) -> bool:
106+
return self.name == "try"
107+
104108
# TODO: help text for fields below.
105109
name = models.CharField(max_length=255, unique=True)
106110
default_branch = models.CharField(max_length=255, default="", blank=True)

src/lando/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
LandingJobPullRequestAPIView,
2424
LegacyDiffWarningView,
2525
PullRequestChecksAPIView,
26+
PullRequestTryPushAPIView,
2627
git2hgCommitMapView,
2728
hg2gitCommitMapView,
2829
)
@@ -103,6 +104,11 @@
103104
LandingJobPullRequestAPIView.as_view(),
104105
name="api-landing-job-pull-request",
105106
),
107+
path(
108+
"api/pulls/<str:repo_name>/<int:pull_number>/try_jobs",
109+
PullRequestTryPushAPIView.as_view(),
110+
name="api-try-job-pull-request",
111+
),
106112
path(
107113
"api/pulls/<str:repo_name>/<int:number>/checks",
108114
PullRequestChecksAPIView.as_view(),

0 commit comments

Comments
 (0)