From f4aa89022d48319947e05fa08c5f7a95a1506903 Mon Sep 17 00:00:00 2001 From: Nicolas Catoni Date: Wed, 30 Sep 2026 13:58:43 +0200 Subject: [PATCH 1/2] Sign agent update commits --- .../test_update_agent_version.py | 65 ++++++++++++++++++- .../ci/gitlab/docker/system-tests.Dockerfile | 4 ++ utils/ci/gitlab/main.yml | 2 +- utils/scripts/update_agent_version.py | 35 ++++++++-- 4 files changed, 96 insertions(+), 10 deletions(-) diff --git a/tests/test_the_test/test_update_agent_version.py b/tests/test_the_test/test_update_agent_version.py index 351219fc3ba..770661cdb5e 100644 --- a/tests/test_the_test/test_update_agent_version.py +++ b/tests/test_the_test/test_update_agent_version.py @@ -1,5 +1,6 @@ import os import subprocess +import urllib.error from pathlib import Path from utils import pytest @@ -13,6 +14,7 @@ enable_auto_merge, normalize_version, publish_update, + push_signed_commit, revoke_token, run_automation, update_agent_version, @@ -194,16 +196,36 @@ def fake_run( ) -> subprocess.CompletedProcess[str]: assert env == {"GH_TOKEN": "token"} commands.append(args) - return subprocess.CompletedProcess(args, 0, stdout="" if capture_output else None) + if not capture_output: + return subprocess.CompletedProcess(args, 0, stdout=None) + if args == ["git", "rev-parse", "HEAD"]: + return subprocess.CompletedProcess(args, 0, stdout="main-sha\n") + if args[0] == "commit-headless": + return subprocess.CompletedProcess(args, 0, stdout="signed-sha\n") + return subprocess.CompletedProcess(args, 0, stdout="") monkeypatch.setattr("utils.scripts.update_agent_version.run_command", fake_run) github = FakeGitHubApi() publish_update(tmp_path, "7.82.3", github, {"GH_TOKEN": "token"}) + assert ["git", "rev-parse", "HEAD"] in commands assert ["git", "add", str(AGENT_VERSION_LOCK)] in commands - assert ["git", "push", "--force", "--set-upstream", "origin", AUTOMATION_BRANCH] in commands + # The fake GitHub API never raises a 404 for the branch-existence check, so the branch is + # always treated as already existing and force-updated. + assert [ + "commit-headless", + "push", + "-T", + "DataDog/system-tests", + "--branch", + AUTOMATION_BRANCH, + "--head-sha", + "main-sha", + "--force", + ] in commands assert not any(command[0] == "gh" for command in commands) + assert any(method == "GET" and path == f"/repos/DataDog/system-tests/git/ref/heads/{AUTOMATION_BRANCH}" for method, path in github.calls) assert any(method == "POST" and path.endswith("/pulls") for method, path in github.calls) is (not existing_pr) if existing_pr: assert ("PATCH", f"/repos/DataDog/system-tests/pulls/{existing_pr}") in github.calls @@ -212,6 +234,45 @@ def fake_run( assert github.calls[-1] == ("POST", "/graphql") +@scenarios.test_the_test +def test_push_signed_commit_creates_branch_when_missing(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + commands: list[list[str]] = [] + + class MissingBranchGitHubApi(GitHubApi): + def __init__(self) -> None: + super().__init__("token") + + def request(self, method: str, path: str, data: dict[str, object] | None = None) -> object: # noqa: ARG002 + raise urllib.error.HTTPError(path, 404, "Not Found", {}, None) # type: ignore[arg-type] + + def fake_run( + _root: Path, + args: list[str], + *, + capture_output: bool = False, # noqa: ARG001 + env: dict[str, str] | None = None, # noqa: ARG001 + ) -> subprocess.CompletedProcess[str]: + commands.append(args) + return subprocess.CompletedProcess(args, 0, stdout="signed-sha\n") + + monkeypatch.setattr("utils.scripts.update_agent_version.run_command", fake_run) + + signed_sha = push_signed_commit(tmp_path, "main-sha", MissingBranchGitHubApi(), {"GH_TOKEN": "token"}) + + assert signed_sha == "signed-sha" + assert [ + "commit-headless", + "push", + "-T", + "DataDog/system-tests", + "--branch", + AUTOMATION_BRANCH, + "--head-sha", + "main-sha", + "--create-branch", + ] in commands + + def fake_github(result: object) -> GitHubApi: class FakeGitHubApi(GitHubApi): def request(self, method: str, path: str, data: dict[str, object] | None = None) -> object: # noqa: ARG002 diff --git a/utils/ci/gitlab/docker/system-tests.Dockerfile b/utils/ci/gitlab/docker/system-tests.Dockerfile index 213339bea11..0df9ac15080 100644 --- a/utils/ci/gitlab/docker/system-tests.Dockerfile +++ b/utils/ci/gitlab/docker/system-tests.Dockerfile @@ -62,4 +62,8 @@ COPY --from=registry.ddbuild.io/ddsign:v1.11.10@sha256:55784668a612ab22129bb15a6 # For more information see https://datadoghq.atlassian.net/wiki/spaces/SECENG/pages/5138645099/User+guide+dd-octo-sts#%3Agitlab%3A-Via-Gitlab-CI-job COPY --from=registry.ddbuild.io/dd-octo-sts:v1.9.3@sha256:f8412df42db2e1879182c820ea4ef600ab4375c5b696a24151c7f0dd931ffee6 /usr/local/bin/dd-octo-sts /usr/local/bin/dd-octo-sts +# Used to push signed commits to automation branches (e.g. the pinned Agent version update) +# https://github.com/DataDog/commit-headless +COPY --from=registry.ddbuild.io/commit-headless:v3.4.0@sha256:7a7c7853250c0dcd4682548a256d4e470fc4443a9a37e0c69aa83e85e53a1dbc /commit-headless /usr/local/bin/commit-headless + WORKDIR / diff --git a/utils/ci/gitlab/main.yml b/utils/ci/gitlab/main.yml index 807c87fdaa7..17415be6f5b 100644 --- a/utils/ci/gitlab/main.yml +++ b/utils/ci/gitlab/main.yml @@ -83,7 +83,7 @@ variables: # Tag = first 12 chars of sha256(utils/ci/gitlab/docker/system-tests.Dockerfile + requirements.txt) # Update this when either file changes: # cat utils/ci/gitlab/docker/system-tests.Dockerfile requirements.txt | sha256sum | cut -c1-12 - CI_IMAGE: "registry.ddbuild.io/system-tests/ci-runner:65e2f06534fd" + CI_IMAGE: "registry.ddbuild.io/system-tests/ci-runner:970b845364b9" SYSTEM_TESTS_SPLIT_PIPELINE: "$[[ inputs.split_pipeline ]]" .system_tests_param_base: diff --git a/utils/scripts/update_agent_version.py b/utils/scripts/update_agent_version.py index 719aec1d6d6..90850fb618d 100755 --- a/utils/scripts/update_agent_version.py +++ b/utils/scripts/update_agent_version.py @@ -7,6 +7,7 @@ import os import re import subprocess +import urllib.error import urllib.parse import urllib.request from pathlib import Path @@ -115,19 +116,39 @@ def enable_auto_merge(github: GitHubApi, pull_request_node_id: str) -> None: raise RuntimeError("GitHub failed to enable pull request auto-merge") +def remote_branch_exists(github: GitHubApi) -> bool: + try: + github.request("GET", f"/repos/{REPOSITORY}/git/ref/heads/{AUTOMATION_BRANCH}") + except urllib.error.HTTPError as error: + if error.code == 404: + return False + raise + return True + + +def push_signed_commit(root: Path, base_sha: str, github: GitHubApi, env: Mapping[str, str]) -> str: + # The branch is (re)created from main on every run, so its remote counterpart, if any, always + # needs a force-update: only its very first push can fast-forward via --create-branch. + flag = "--force" if remote_branch_exists(github) else "--create-branch" + result = run_command( + root, + ["commit-headless", "push", "-T", REPOSITORY, "--branch", AUTOMATION_BRANCH, "--head-sha", base_sha, flag], + capture_output=True, + env=env, + ) + return result.stdout.strip() + + def publish_update(root: Path, version: str, github: GitHubApi, env: Mapping[str, str]) -> None: - run_command(root, ["git", "remote", "set-url", "origin", f"https://github.com/{REPOSITORY}.git"], env=env) + base_sha = run_command(root, ["git", "rev-parse", "HEAD"], capture_output=True, env=env).stdout.strip() run_command(root, ["git", "switch", "--force-create", AUTOMATION_BRANCH], env=env) run_command(root, ["git", "add", str(AGENT_VERSION_LOCK)], env=env) run_command(root, ["git", "config", "user.name", "github-actions[bot]"], env=env) run_command(root, ["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"], env=env) - run_command( - root, - ["git", "config", "credential.helper", "!f() { echo username=x-access-token; echo password=$GH_TOKEN; }; f"], - env=env, - ) run_command(root, ["git", "commit", "-m", f"Update Agent to {version}"], env=env) - run_command(root, ["git", "push", "--force", "--set-upstream", "origin", AUTOMATION_BRANCH], env=env) + + signed_sha = push_signed_commit(root, base_sha, github, env) + print(f"Pushed signed commit {signed_sha} to {AUTOMATION_BRANCH}") head = urllib.parse.quote(f"DataDog:{AUTOMATION_BRANCH}", safe="") pull_requests = github.request("GET", f"/repos/{REPOSITORY}/pulls?head={head}&state=open") From 388050d42a8ca060d2de576f877421c3da9a6be6 Mon Sep 17 00:00:00 2001 From: Nicolas Catoni Date: Wed, 30 Sep 2026 14:06:46 +0200 Subject: [PATCH 2/2] Fix lint --- .../test_update_agent_version.py | 5 ++++- utils/scripts/update_agent_version.py | 21 ++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/tests/test_the_test/test_update_agent_version.py b/tests/test_the_test/test_update_agent_version.py index 770661cdb5e..d00e8b2320b 100644 --- a/tests/test_the_test/test_update_agent_version.py +++ b/tests/test_the_test/test_update_agent_version.py @@ -225,7 +225,10 @@ def fake_run( "--force", ] in commands assert not any(command[0] == "gh" for command in commands) - assert any(method == "GET" and path == f"/repos/DataDog/system-tests/git/ref/heads/{AUTOMATION_BRANCH}" for method, path in github.calls) + assert any( + method == "GET" and path == f"/repos/DataDog/system-tests/git/ref/heads/{AUTOMATION_BRANCH}" + for method, path in github.calls + ) assert any(method == "POST" and path.endswith("/pulls") for method, path in github.calls) is (not existing_pr) if existing_pr: assert ("PATCH", f"/repos/DataDog/system-tests/pulls/{existing_pr}") in github.calls diff --git a/utils/scripts/update_agent_version.py b/utils/scripts/update_agent_version.py index 90850fb618d..ae5a0db3192 100755 --- a/utils/scripts/update_agent_version.py +++ b/utils/scripts/update_agent_version.py @@ -120,7 +120,8 @@ def remote_branch_exists(github: GitHubApi) -> bool: try: github.request("GET", f"/repos/{REPOSITORY}/git/ref/heads/{AUTOMATION_BRANCH}") except urllib.error.HTTPError as error: - if error.code == 404: + not_found = 404 + if error.code == not_found: return False raise return True @@ -132,7 +133,17 @@ def push_signed_commit(root: Path, base_sha: str, github: GitHubApi, env: Mappin flag = "--force" if remote_branch_exists(github) else "--create-branch" result = run_command( root, - ["commit-headless", "push", "-T", REPOSITORY, "--branch", AUTOMATION_BRANCH, "--head-sha", base_sha, flag], + [ + "commit-headless", + "push", + "-T", + REPOSITORY, + "--branch", + AUTOMATION_BRANCH, + "--head-sha", + base_sha, + flag, + ], capture_output=True, env=env, ) @@ -144,7 +155,11 @@ def publish_update(root: Path, version: str, github: GitHubApi, env: Mapping[str run_command(root, ["git", "switch", "--force-create", AUTOMATION_BRANCH], env=env) run_command(root, ["git", "add", str(AGENT_VERSION_LOCK)], env=env) run_command(root, ["git", "config", "user.name", "github-actions[bot]"], env=env) - run_command(root, ["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"], env=env) + run_command( + root, + ["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"], + env=env, + ) run_command(root, ["git", "commit", "-m", f"Update Agent to {version}"], env=env) signed_sha = push_signed_commit(root, base_sha, github, env)