Skip to content

Commit 4286c03

Browse files
authored
Builds: support custom Git checkout command (#12412)
Initial POC to support this feature. It will allow us to support customers with specific needs. We don't plan to expose this directly to users yet, since there are some internals that need to be met to work: - Use specific environment variables - Clone the repository in `.` to keep with the default workflow - Depending on the repository structure, a custom YAML path and custom `python.install.requirements` may be needed as well (I added this commit humitos/rocm-libraries@a393812) All of this can be documented eventually, but we need an internal way to write these custom commands to onboard customers with these needs. <img width="1161" height="518" alt="Screenshot_2025-08-15_12-06-48" src="https://github.com/user-attachments/assets/b95c5007-5346-4539-883b-58546ee15d4f" /> The value for `Project.git_checkout_command` is: ```json [ "env", "echo $READTHEDOCS_GIT_CLONE_URL", "git clone --no-checkout --no-tag --filter=blob:none --depth 1 $READTHEDOCS_GIT_CLONE_URL .", "git sparse-checkout init --cone", "git sparse-checkout set projects/rocblas", "git checkout $READTHEDOCS_GIT_IDENTIFIER" ] ``` Closes #12313
1 parent 02dc6ae commit 4286c03

File tree

6 files changed

+94
-0
lines changed

6 files changed

+94
-0
lines changed

readthedocs/api/v2/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class Meta(ProjectSerializer.Meta):
9595
"readthedocs_yaml_path",
9696
"clone_token",
9797
"has_ssh_key_with_write_access",
98+
"git_checkout_command",
9899
)
99100

100101

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 5.2.4 on 2025-08-15 09:07
2+
3+
from django.db import migrations
4+
from django.db import models
5+
from django_safemigrate import Safe
6+
7+
8+
class Migration(migrations.Migration):
9+
safe = Safe.before_deploy()
10+
11+
dependencies = [
12+
("projects", "0154_set_latest_build"),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name="historicalproject",
18+
name="git_checkout_command",
19+
field=models.JSONField(
20+
blank=True, null=True, verbose_name="Custom command to execute before Git checkout"
21+
),
22+
),
23+
migrations.AddField(
24+
model_name="project",
25+
name="git_checkout_command",
26+
field=models.JSONField(
27+
blank=True, null=True, verbose_name="Custom command to execute before Git checkout"
28+
),
29+
),
30+
]

readthedocs/projects/models.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,18 @@ class Project(models.Model):
300300
blank=True,
301301
help_text=_("Short description of this project"),
302302
)
303+
304+
# Example:
305+
# [
306+
# "git clone --no-checkout --no-tag --filter=blob:none --depth 1 $READTHEDOCS_GIT_CLONE_URL .",
307+
# "git checkout $READTHEDOCS_GIT_IDENTIFIER"
308+
# ]
309+
git_checkout_command = models.JSONField(
310+
_("Custom command to execute before Git checkout"),
311+
null=True,
312+
blank=True,
313+
)
314+
303315
repo = models.CharField(
304316
_("Repository URL"),
305317
max_length=255,

readthedocs/projects/tests/test_build_tasks.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,6 +1655,42 @@ def test_build_commands_executed_with_clone_token(
16551655
]
16561656
)
16571657

1658+
@mock.patch("readthedocs.doc_builder.director.load_yaml_config")
1659+
def test_project_with_custom_git_checkout_command(self, load_yaml_config):
1660+
git_checkout_command = [
1661+
"env",
1662+
"echo $READTHEDOCS_GIT_CLONE_URL",
1663+
"git clone --no-checkout --no-tag --filter=blob:none --depth 1 $READTHEDOCS_GIT_CLONE_URL .",
1664+
"git sparse-checkout init --cone",
1665+
"git sparse-checkout set projects/project",
1666+
"git checkout $READTHEDOCS_GIT_IDENTIFIER" ,
1667+
]
1668+
self.project.git_checkout_command = git_checkout_command
1669+
self.project.save()
1670+
1671+
config = BuildConfigV2(
1672+
{
1673+
"version": 2,
1674+
"build": {
1675+
"os": "ubuntu-22.04",
1676+
"tools": {
1677+
"python": "3",
1678+
},
1679+
},
1680+
},
1681+
source_file="readthedocs.yml",
1682+
)
1683+
config.validate()
1684+
load_yaml_config.return_value = config
1685+
1686+
self._trigger_update_docs_task()
1687+
1688+
self.mocker.mocks["git.Backend.run"].assert_has_calls(
1689+
[
1690+
mock.call(*cmd.split(), escape_command=False) for cmd in git_checkout_command
1691+
]
1692+
)
1693+
16581694
@mock.patch("readthedocs.doc_builder.director.load_yaml_config")
16591695
def test_install_apt_packages(self, load_yaml_config):
16601696
config = BuildConfigV2(

readthedocs/rtd_tests/tests/test_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3425,6 +3425,7 @@ def test_get_version_by_id(self):
34253425
"documentation_type": "sphinx",
34263426
"environment_variables": {},
34273427
"features": [],
3428+
"git_checkout_command": None,
34283429
"has_valid_clone": False,
34293430
"has_valid_webhook": False,
34303431
"id": 6,

readthedocs/vcs_support/backends/git.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ def update(self):
4444
"""Clone and/or fetch remote repository."""
4545
super().update()
4646

47+
if self.project.git_checkout_command:
48+
# Run custom checkout step if defined
49+
if isinstance(self.project.git_checkout_command, list):
50+
for cmd in self.project.git_checkout_command:
51+
# NOTE: we need to pass ``escape_command=False`` here to be
52+
# able to expand environment variables.
53+
code, stdout, stderr = self.run(*cmd.split(), escape_command=False)
54+
return
55+
4756
# Check for existing checkout and skip clone if it exists.
4857
from readthedocs.projects.models import Feature
4958

@@ -500,6 +509,11 @@ def checkout(self, identifier=None):
500509
"""Checkout to identifier or latest."""
501510
super().checkout()
502511

512+
# Do not checkout anything else if the project has a custom Git checkout command.
513+
# The ``git checkout`` command has to be executed inside the ``update()`` method.
514+
if self.project.git_checkout_command:
515+
return
516+
503517
# NOTE: if there is no identifier, we default to default branch cloned
504518
if not identifier:
505519
return

0 commit comments

Comments
 (0)