Skip to content

Commit 7005266

Browse files
authored
Allow release tagging before agent branch exists (DataDog#23711)
1 parent 1e1a029 commit 7005266

6 files changed

Lines changed: 221 additions & 84 deletions

File tree

.github/workflows/update-build-agent-yaml.yml

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,48 @@ jobs:
2323
with:
2424
ref: ${{ inputs.branch }}
2525

26-
- name: Verify preconditions
26+
- name: Update build_agent.yaml
2727
run: |-
28-
if ! grep -qE '^\s+branch:\s+main\s*$' .gitlab/build_agent.yaml; then
29-
echo "build_agent.yaml already points to the release branch — nothing to do."
28+
python3 - <<'PY'
29+
import os
30+
import re
31+
from pathlib import Path
32+
33+
path = Path(".gitlab/build_agent.yaml")
34+
content = path.read_text()
35+
template_regex = re.compile(r"^\.build-agent-tpl:\n(?:[^\S\n].*(?:\n|$))*", re.MULTILINE)
36+
branch_regex = re.compile(r"^(\s+branch:\s+)main([^\S\n]*)$", re.MULTILINE)
37+
38+
template_match = template_regex.search(content)
39+
if template_match is None:
40+
print("build_agent.yaml does not contain `.build-agent-tpl` — nothing to update.")
41+
raise SystemExit(0)
42+
43+
template = template_match.group(0)
44+
matches = list(branch_regex.finditer(template))
45+
if not matches:
46+
print("`.build-agent-tpl` does not contain a branch entry pointing to main — nothing to update.")
47+
raise SystemExit(0)
48+
if len(matches) != 1:
49+
print(f"::error::Expected exactly one `.build-agent-tpl` branch pointing to main; found {len(matches)}.")
50+
raise SystemExit(1)
51+
52+
def replacement(match):
53+
return f"{match.group(1)}{os.environ['BRANCH']}{match.group(2)}"
54+
55+
updated_template, replacement_count = branch_regex.subn(replacement, template, count=1)
56+
assert replacement_count == 1
57+
path.write_text(content[: template_match.start()] + updated_template + content[template_match.end() :])
58+
PY
59+
60+
if git diff --quiet -- .gitlab/build_agent.yaml; then
3061
exit 0
3162
fi
3263
if ! git ls-remote --exit-code --heads https://github.com/DataDog/datadog-agent.git "$BRANCH" >/dev/null 2>&1; then
33-
echo "::error::Agent branch '$BRANCH' does not exist in DataDog/datadog-agent yet."
34-
exit 1
64+
echo "::warning::Agent branch '$BRANCH' does not exist in DataDog/datadog-agent yet. Creating the PR anyway."
3565
fi
3666
echo "needs_update=true" >> "$GITHUB_ENV"
3767
38-
- name: Update build_agent.yaml
39-
if: env.needs_update == 'true'
40-
run: |-
41-
sed -i "s/^ branch: main$/ branch: $BRANCH/" .gitlab/build_agent.yaml
42-
4368
- name: Get token via dd-octo-sts
4469
if: env.needs_update == 'true'
4570
uses: DataDog/dd-octo-sts-action@96a25462dbcb10ebf0bfd6e2ccc917d2ab235b9a # v1.0.4

ddev/changelog.d/23711.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow release branch tagging to continue before the matching Agent branch exists.

ddev/src/ddev/cli/release/branch/create.py

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717

1818
BRANCH_NAME_PATTERN = r"^\d+\.\d+\.x$"
1919
BRANCH_NAME_REGEX = re.compile(BRANCH_NAME_PATTERN)
20+
BUILD_AGENT_YAML_PATH = '.gitlab/build_agent.yaml'
21+
BUILD_AGENT_TEMPLATE_PATTERN = r'^\.build-agent-tpl:\n(?:[^\S\n].*(?:\n|$))*'
22+
BUILD_AGENT_MAIN_BRANCH_PATTERN = r'^(\s+branch:\s+)main([^\S\n]*)$'
23+
BUILD_AGENT_TEMPLATE_REGEX = re.compile(BUILD_AGENT_TEMPLATE_PATTERN, re.MULTILINE)
24+
BUILD_AGENT_MAIN_BRANCH_REGEX = re.compile(BUILD_AGENT_MAIN_BRANCH_PATTERN, re.MULTILINE)
2025
GITHUB_LABEL_COLOR = '5319e7'
21-
DATADOG_AGENT_REPO_URL = 'https://github.com/DataDog/datadog-agent.git'
2226

2327

2428
@click.command
@@ -178,58 +182,59 @@ def suggest_next_branch(app: Application) -> str:
178182

179183

180184
def ensure_build_agent_yaml_updated(app: Application, branch_name: str) -> bool:
181-
"""
182-
Ensure build_agent.yaml points to the correct agent branch for release builds.
183-
184-
This function:
185-
1. Checks if the file still points to 'main' (needs update)
186-
2. Checks if the agent branch exists in datadog-agent repository
187-
3. Updates the file if both conditions are met
188-
189-
Args:
190-
branch_name: The release branch name (e.g., '7.45.x')
191-
192-
Returns:
193-
True if the file was updated, False otherwise.
194-
"""
185+
"""Update build_agent.yaml to point to the release branch when it still targets main."""
195186
from ddev.utils.fs import Path
196187

197-
build_agent_yaml = Path('.gitlab/build_agent.yaml')
188+
build_agent_yaml = Path(BUILD_AGENT_YAML_PATH)
198189

199190
if not build_agent_yaml.exists():
200191
app.display_warning(f'Warning: {build_agent_yaml} not found')
201192
return False
202193

203-
# Read the current content
204194
with open(build_agent_yaml, 'r') as f:
205195
content = f.read()
206196

207-
# Check if file still points to main (needs update)
208-
old_pattern = r'(\s+branch:\s+)main'
209-
if not re.search(old_pattern, content):
210-
# Already updated to a release branch, nothing to do
197+
matches = find_build_agent_template_main_branch_matches(content)
198+
if not matches:
211199
return False
212-
213-
# Check if the agent branch exists in datadog-agent repository using git ls-remote
214-
app.display_waiting(f'Checking if branch `{branch_name}` exists in datadog-agent...')
215-
ls_remote_output = app.repo.git.capture('ls-remote', '--heads', DATADOG_AGENT_REPO_URL, branch_name)
216-
if not ls_remote_output.strip():
217-
app.display_warning(
218-
f"Agent branch `{branch_name}` does not exist yet in datadog-agent. "
219-
f"Keeping build_agent.yaml pointing to 'main'. "
220-
f"The `update-build-agent-yaml` workflow will create a PR to update the file "
221-
f"once the agent branch is created."
200+
if len(matches) > 1:
201+
app.abort(
202+
f'Expected exactly one `.build-agent-tpl` branch pointing to `main` in `{BUILD_AGENT_YAML_PATH}`; '
203+
f'found {len(matches)}.'
222204
)
223205
return False
224206

225-
# Agent branch exists, update the file
226-
def replacement(match):
227-
return match.group(1) + branch_name
228-
229-
updated_content = re.sub(old_pattern, replacement, content)
207+
updated_content, replacement_count = replace_build_agent_template_main_branch(content, branch_name)
208+
assert replacement_count == 1
230209

231210
with open(build_agent_yaml, 'w') as f:
232211
f.write(updated_content)
233212

234213
app.display_success(f'Updated build_agent.yaml file to use Agent branch: {branch_name}')
235214
return True
215+
216+
217+
def find_build_agent_template_main_branch_matches(content: str) -> list[re.Match[str]]:
218+
template_match = BUILD_AGENT_TEMPLATE_REGEX.search(content)
219+
if template_match is None:
220+
return []
221+
222+
return list(BUILD_AGENT_MAIN_BRANCH_REGEX.finditer(template_match.group(0)))
223+
224+
225+
def replace_build_agent_template_main_branch(content: str, branch_name: str) -> tuple[str, int]:
226+
template_match = BUILD_AGENT_TEMPLATE_REGEX.search(content)
227+
if template_match is None:
228+
return content, 0
229+
230+
def replacement(match: re.Match[str]) -> str:
231+
return f'{match.group(1)}{branch_name}{match.group(2)}'
232+
233+
updated_template, replacement_count = BUILD_AGENT_MAIN_BRANCH_REGEX.subn(
234+
replacement, template_match.group(0), count=1
235+
)
236+
if replacement_count == 0:
237+
return content, 0
238+
239+
updated_content = content[: template_match.start()] + updated_template + content[template_match.end() :]
240+
return updated_content, replacement_count

ddev/src/ddev/cli/release/branch/tag.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1+
from __future__ import annotations
2+
13
import logging
24
import re
5+
from typing import TYPE_CHECKING
36

47
import click
8+
from httpx import HTTPStatusError
59
from packaging.version import Version
610

7-
from .create import BRANCH_NAME_REGEX
11+
from .create import BRANCH_NAME_REGEX, BUILD_AGENT_YAML_PATH, find_build_agent_template_main_branch_matches
12+
13+
if TYPE_CHECKING:
14+
from ddev.cli.application import Application
15+
16+
UPDATE_BUILD_AGENT_YAML_WORKFLOW = 'update-build-agent-yaml.yml'
17+
# The dd-octo-sts policy grants PR-writing credentials only to this workflow on master.
18+
UPDATE_BUILD_AGENT_YAML_WORKFLOW_REF = 'master'
819

920

1021
@click.command
@@ -36,12 +47,14 @@ def tag(app, final: bool, skip_open_pr_check: bool):
3647
click.echo(app.repo.git.pull(branch_name))
3748
click.echo(app.repo.git.fetch_tags())
3849

39-
if _build_agent_yaml_points_to_main():
40-
app.abort(
50+
build_agent_yaml_needs_update = _build_agent_yaml_points_to_main()
51+
# Recovery path for release branches cut before build_agent.yaml was updated.
52+
if build_agent_yaml_needs_update:
53+
app.display_warning(
4154
"`.gitlab/build_agent.yaml` still points to `main`.\n"
42-
"The agent branch may not exist yet in datadog-agent, or the update PR hasn't been merged.\n"
43-
f"To trigger the workflow manually: gh workflow run update-build-agent-yaml.yml -f branch={branch_name}\n"
44-
"Once the PR is merged, re-run this command."
55+
"The update PR may not have been created or merged yet.\n"
56+
"Will trigger the workflow after the tag is pushed.\n"
57+
"Tagging will continue."
4558
)
4659

4760
major_minor_version = branch_name.replace('.x', '')
@@ -105,13 +118,33 @@ def tag(app, final: bool, skip_open_pr_check: bool):
105118
app.abort('Did not get confirmation, aborting. Did not create or push the tag.')
106119
click.echo(app.repo.git.tag(new_tag, message=new_tag))
107120
click.echo(app.repo.git.push(new_tag))
121+
if build_agent_yaml_needs_update:
122+
_trigger_build_agent_yaml_update_workflow(app, branch_name)
108123

109124

110125
def _build_agent_yaml_points_to_main() -> bool:
111126
from ddev.utils.fs import Path
112127

113-
path = Path('.gitlab/build_agent.yaml')
114-
return path.exists() and bool(re.search(r'\s+branch:\s+main$', path.read_text(), re.MULTILINE))
128+
path = Path(BUILD_AGENT_YAML_PATH)
129+
return path.exists() and bool(find_build_agent_template_main_branch_matches(path.read_text()))
130+
131+
132+
def _trigger_build_agent_yaml_update_workflow(app: Application, branch_name: str) -> None:
133+
try:
134+
app.github.dispatch_workflow(
135+
UPDATE_BUILD_AGENT_YAML_WORKFLOW,
136+
UPDATE_BUILD_AGENT_YAML_WORKFLOW_REF,
137+
{'branch': branch_name},
138+
)
139+
except HTTPStatusError as e:
140+
app.display_warning(
141+
f'Warning: unable to trigger `{UPDATE_BUILD_AGENT_YAML_WORKFLOW}`: {e}\n'
142+
f'To trigger it manually: gh workflow run {UPDATE_BUILD_AGENT_YAML_WORKFLOW} -f branch={branch_name}'
143+
)
144+
else:
145+
app.display_success(
146+
f'Dispatched `{UPDATE_BUILD_AGENT_YAML_WORKFLOW}`; check the workflow run for PR creation status.'
147+
)
115148

116149

117150
def _extract_patch_and_rc(version_tags):

ddev/tests/cli/release/branch/test_create.py

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ def test_create_invalid_branch_name(ddev, name, mocker):
3232
@pytest.mark.parametrize(
3333
'yaml_updated',
3434
[
35-
pytest.param(True, id='agent_branch_exists'),
36-
pytest.param(False, id='agent_branch_not_exists'),
35+
pytest.param(True, id='build_agent_yaml_updated'),
36+
pytest.param(False, id='build_agent_yaml_unchanged'),
3737
],
3838
)
3939
def test_create_branch(ddev, mocker, yaml_updated):
@@ -58,7 +58,7 @@ def test_create_branch(ddev, mocker, yaml_updated):
5858
run_mock.assert_any_call('checkout', '-B', '5.5.x')
5959
run_mock.assert_any_call('push', 'origin', '5.5.x')
6060

61-
# yaml commit only happens when agent branch exists
61+
# yaml commit only happens when build_agent.yaml was updated
6262
yaml_commit = call('add', '.gitlab/build_agent.yaml')
6363
assert (yaml_commit in run_mock.call_args_list) is yaml_updated
6464

@@ -78,45 +78,73 @@ def test_create_branch(ddev, mocker, yaml_updated):
7878
assert run_mock.call_args_list[-1] == call('checkout', 'master')
7979

8080

81-
@pytest.mark.parametrize(
82-
'ls_remote_output,expected_result,file_should_change',
83-
[
84-
pytest.param('abc123\trefs/heads/7.99.x\n', True, True, id='branch_exists'),
85-
pytest.param('', False, False, id='branch_not_exists'),
86-
],
87-
)
88-
def test_ensure_build_agent_yaml_updated(mocker, tmp_path, ls_remote_output, expected_result, file_should_change):
89-
"""Test ensure_build_agent_yaml_updated with different branch existence scenarios."""
81+
def test_ensure_build_agent_yaml_updated(mocker, tmp_path):
9082
build_agent_path = Path(tmp_path / '.gitlab' / 'build_agent.yaml')
9183
build_agent_path.parent.ensure_dir_exists()
9284
build_agent_path.write_text('.build-agent-tpl:\n trigger:\n branch: main\n')
9385

9486
app_mock = mocker.MagicMock()
95-
app_mock.repo.git.capture.return_value = ls_remote_output
9687

9788
with Path(tmp_path).as_cwd():
9889
result = ensure_build_agent_yaml_updated(app_mock, '7.99.x')
9990

100-
assert result is expected_result
101-
content = build_agent_path.read_text()
102-
if file_should_change:
103-
assert 'branch: 7.99.x' in content
104-
else:
105-
assert 'branch: main' in content
91+
assert result is True
92+
assert 'branch: 7.99.x' in build_agent_path.read_text()
93+
app_mock.repo.git.capture.assert_not_called()
94+
95+
96+
def test_ensure_build_agent_yaml_updated_ignores_unrelated_main_branch(mocker, tmp_path):
97+
build_agent_path = Path(tmp_path / '.gitlab' / 'build_agent.yaml')
98+
build_agent_path.parent.ensure_dir_exists()
99+
content = '.build-agent-tpl:\n trigger:\n branch: main\nunrelated-job:\n trigger:\n branch: main\n'
100+
build_agent_path.write_text(content)
101+
102+
app_mock = mocker.MagicMock()
103+
104+
with Path(tmp_path).as_cwd():
105+
result = ensure_build_agent_yaml_updated(app_mock, '7.99.x')
106+
107+
assert result is True
108+
assert build_agent_path.read_text() == (
109+
'.build-agent-tpl:\n trigger:\n branch: 7.99.x\nunrelated-job:\n trigger:\n branch: main\n'
110+
)
111+
app_mock.abort.assert_not_called()
112+
113+
114+
def test_ensure_build_agent_yaml_updated_aborts_on_multiple_template_main_branches(mocker, tmp_path):
115+
build_agent_path = Path(tmp_path / '.gitlab' / 'build_agent.yaml')
116+
build_agent_path.parent.ensure_dir_exists()
117+
content = '.build-agent-tpl:\n trigger:\n branch: main\n branch: main\n'
118+
build_agent_path.write_text(content)
119+
120+
app_mock = mocker.MagicMock()
121+
app_mock.abort.side_effect = RuntimeError('abort')
122+
123+
with Path(tmp_path).as_cwd(), pytest.raises(RuntimeError, match='abort'):
124+
ensure_build_agent_yaml_updated(app_mock, '7.99.x')
125+
126+
assert build_agent_path.read_text() == content
127+
app_mock.abort.assert_called_once_with(
128+
'Expected exactly one `.build-agent-tpl` branch pointing to `main` in `.gitlab/build_agent.yaml`; found 2.'
129+
)
106130

107131

108132
def test_ensure_build_agent_yaml_updated_already_on_release_branch(mocker, tmp_path):
109133
"""Test early return when file already points to a release branch."""
110134
build_agent_path = Path(tmp_path / '.gitlab' / 'build_agent.yaml')
111135
build_agent_path.parent.ensure_dir_exists()
112-
build_agent_path.write_text('.build-agent-tpl:\n trigger:\n branch: 7.98.x\n')
136+
build_agent_path.write_text(
137+
'.build-agent-tpl:\n trigger:\n branch: 7.98.x\nunrelated-job:\n trigger:\n branch: main\n'
138+
)
113139

114140
app_mock = mocker.MagicMock()
115141

116142
with Path(tmp_path).as_cwd():
117143
result = ensure_build_agent_yaml_updated(app_mock, '7.99.x')
118144

119145
assert result is False
146+
assert 'branch: 7.98.x' in build_agent_path.read_text()
147+
assert 'branch: main' in build_agent_path.read_text()
120148
app_mock.repo.git.capture.assert_not_called()
121149

122150

0 commit comments

Comments
 (0)