Skip to content

Commit f2a5f70

Browse files
authored
feat(ci_visibility): add github action tags for attempt to fix workflow (#14099)
## What? - Add a tag for the head commit sha if we run inside a GitHub Action - Whenever the HEAD commit SHA is present in the tags that come from the CI provider, we assume that the CI provider added a commit on top of the user's HEAD commit (e.g., GitHub Actions add a merge commit when triggered by a pull request). In that case, we extract the metadata for that commit specifically and add it to the tags. - These metadata are needed for Test Optimization's attempt to fix workflow ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
1 parent a23d875 commit f2a5f70

File tree

4 files changed

+158
-12
lines changed

4 files changed

+158
-12
lines changed

ddtrace/contrib/internal/pytest/_plugin_v2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ def pytest_sessionstart(session: pytest.Session) -> None:
345345
test_impact_analysis="1" if _pytest_version_supports_itr() else None,
346346
test_management_quarantine="1",
347347
test_management_disable="1",
348-
test_management_attempt_to_fix="4" if _pytest_version_supports_attempt_to_fix() else None,
348+
test_management_attempt_to_fix="5" if _pytest_version_supports_attempt_to_fix() else None,
349349
)
350350

351351
InternalTestSession.discover(

ddtrace/ext/ci.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ def tags(env=None, cwd=None):
105105
break
106106

107107
git_info = git.extract_git_metadata(cwd=cwd)
108+
109+
# Whenever the HEAD commit SHA is present in the tags that come from the CI provider, we assume that
110+
# the CI provider added a commit on top of the user's HEAD commit (e.g., GitHub Actions add a merge
111+
# commit when triggered by a pull request). In that case, we extract the metadata for that commit specifically
112+
# and add it to the tags.
113+
head_commit_sha = tags.get(git.COMMIT_HEAD_SHA)
114+
if head_commit_sha:
115+
git_head_info = git.extract_git_head_metadata(head_commit_sha=head_commit_sha, cwd=cwd)
116+
git_info.update(git_head_info)
117+
108118
try:
109119
git_info[WORKSPACE_PATH] = git.extract_workspace_path(cwd=cwd)
110120
except git.GitNotFoundError:
@@ -349,6 +359,15 @@ def extract_github_actions(env):
349359
github_run_id,
350360
)
351361

362+
git_commit_head_sha = None
363+
if "GITHUB_EVENT_PATH" in env:
364+
try:
365+
with open(env["GITHUB_EVENT_PATH"]) as f:
366+
github_event_data = json.load(f)
367+
git_commit_head_sha = github_event_data.get("pull_request", {}).get("head", {}).get("sha")
368+
except Exception as e:
369+
log.error("Failed to read or parse GITHUB_EVENT_PATH: %s", e)
370+
352371
env_vars = {
353372
"GITHUB_SERVER_URL": github_server_url,
354373
"GITHUB_REPOSITORY": github_repository,
@@ -362,6 +381,7 @@ def extract_github_actions(env):
362381
git.BRANCH: env.get("GITHUB_HEAD_REF") or env.get("GITHUB_REF"),
363382
git.COMMIT_SHA: git_commit_sha,
364383
git.REPOSITORY_URL: "{0}/{1}.git".format(github_server_url, github_repository),
384+
git.COMMIT_HEAD_SHA: git_commit_head_sha,
365385
JOB_URL: "{0}/{1}/commit/{2}/checks".format(github_server_url, github_repository, git_commit_sha),
366386
PIPELINE_ID: github_run_id,
367387
PIPELINE_NAME: env.get("GITHUB_WORKFLOW"),

ddtrace/ext/git.py

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,30 @@
3333
# Git Commit SHA
3434
COMMIT_SHA = "git.commit.sha"
3535

36+
# Git Commit HEAD SHA
37+
COMMIT_HEAD_SHA = "git.commit.head.sha"
38+
39+
# Git Commit HEAD message
40+
COMMIT_HEAD_MESSAGE = "git.commit.head.message"
41+
42+
# Git Commit HEAD author date
43+
COMMIT_HEAD_AUTHOR_DATE = "git.commit.head.author.date"
44+
45+
# Git Commit HEAD author email
46+
COMMIT_HEAD_AUTHOR_EMAIL = "git.commit.head.author.email"
47+
48+
# Git Commit HEAD author name
49+
COMMIT_HEAD_AUTHOR_NAME = "git.commit.head.author.name"
50+
51+
# Git Commit HEAD committer date
52+
COMMIT_HEAD_COMMITTER_DATE = "git.commit.head.committer.date"
53+
54+
# Git Commit HEAD committer email
55+
COMMIT_HEAD_COMMITTER_EMAIL = "git.commit.head.committer.email"
56+
57+
# Git Commit HEAD committer name
58+
COMMIT_HEAD_COMMITTER_NAME = "git.commit.head.committer.name"
59+
3660
# Git Repository URL
3761
REPOSITORY_URL = "git.repository_url"
3862

@@ -173,11 +197,12 @@ def _get_device_for_path(path):
173197
return os.stat(path).st_dev
174198

175199

176-
def _unshallow_repository_with_details(cwd=None, repo=None, refspec=None):
177-
# type (Optional[str], Optional[str], Optional[str]) -> _GitSubprocessDetails
200+
def _unshallow_repository_with_details(
201+
cwd: Optional[str] = None, repo: Optional[str] = None, refspec: Optional[str] = None, parent_only: bool = False
202+
) -> _GitSubprocessDetails:
178203
cmd = [
179204
"fetch",
180-
'--shallow-since="1 month ago"',
205+
"--deepen=1" if parent_only else '--shallow-since="1 month ago"',
181206
"--update-shallow",
182207
"--filter=blob:none",
183208
"--recurse-submodules=no",
@@ -190,18 +215,22 @@ def _unshallow_repository_with_details(cwd=None, repo=None, refspec=None):
190215
return _git_subprocess_cmd_with_details(*cmd, cwd=cwd)
191216

192217

193-
def _unshallow_repository(cwd=None, repo=None, refspec=None):
194-
# type (Optional[str], Optional[str], Optional[str]) -> None
195-
_unshallow_repository_with_details(cwd, repo, refspec)
218+
def _unshallow_repository(
219+
cwd: Optional[str] = None,
220+
repo: Optional[str] = None,
221+
refspec: Optional[str] = None,
222+
parent_only: bool = False,
223+
) -> None:
224+
_unshallow_repository_with_details(cwd, repo, refspec, parent_only)
196225

197226

198-
def extract_user_info(cwd=None):
199-
# type: (Optional[str]) -> Dict[str, Tuple[str, str, str]]
227+
def extract_user_info(cwd: Optional[str] = None, commit_sha: Optional[str] = None) -> Dict[str, Tuple[str, str, str]]:
200228
"""Extract commit author info from the git repository in the current directory or one specified by ``cwd``."""
201229
# Note: `git show -s --format... --date...` is supported since git 2.1.4 onwards
202-
stdout = _git_subprocess_cmd(
203-
"show -s --format=%an|||%ae|||%ad|||%cn|||%ce|||%cd --date=format:%Y-%m-%dT%H:%M:%S%z", cwd=cwd
204-
)
230+
cmd = "show -s --format=%an|||%ae|||%ad|||%cn|||%ce|||%cd --date=format:%Y-%m-%dT%H:%M:%S%z"
231+
if commit_sha:
232+
cmd += " " + commit_sha
233+
stdout = _git_subprocess_cmd(cmd=cmd, cwd=cwd)
205234
author_name, author_email, author_date, committer_name, committer_email, committer_date = stdout.split("|||")
206235
return {
207236
"author": (author_name, author_email, author_date),
@@ -316,6 +345,32 @@ def extract_commit_sha(cwd=None):
316345
return commit_sha
317346

318347

348+
def extract_git_head_metadata(head_commit_sha: str, cwd: Optional[str] = None) -> Dict[str, Optional[str]]:
349+
tags: Dict[str, Optional[str]] = {}
350+
351+
is_shallow, *_ = _is_shallow_repository_with_details(cwd=cwd)
352+
if is_shallow:
353+
_unshallow_repository(cwd=cwd, repo=None, refspec=None, parent_only=True)
354+
355+
try:
356+
users = extract_user_info(cwd=cwd, commit_sha=head_commit_sha)
357+
tags[COMMIT_HEAD_AUTHOR_NAME] = users["author"][0]
358+
tags[COMMIT_HEAD_AUTHOR_EMAIL] = users["author"][1]
359+
tags[COMMIT_HEAD_AUTHOR_DATE] = users["author"][2]
360+
tags[COMMIT_HEAD_COMMITTER_NAME] = users["committer"][0]
361+
tags[COMMIT_HEAD_COMMITTER_EMAIL] = users["committer"][1]
362+
tags[COMMIT_HEAD_COMMITTER_DATE] = users["committer"][2]
363+
tags[COMMIT_HEAD_MESSAGE] = _git_subprocess_cmd(" ".join(("log -n 1 --format=%B", head_commit_sha)), cwd)
364+
except GitNotFoundError:
365+
log.error("Git executable not found, cannot extract git metadata.")
366+
except ValueError as e:
367+
debug_mode = log.isEnabledFor(logging.DEBUG)
368+
stderr = str(e)
369+
log.error("Error extracting git metadata: %s", stderr, exc_info=debug_mode)
370+
371+
return tags
372+
373+
319374
def extract_git_metadata(cwd=None):
320375
# type: (Optional[str]) -> Dict[str, Optional[str]]
321376
"""Extract git commit metadata."""

tests/tracer/test_ci.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import glob
33
import json
44
import os
5+
import tempfile
56

67
import mock
78
import pytest
@@ -463,3 +464,73 @@ def test_build_git_packfiles_temp_dir_value_error(_temp_dir_mock, git_repo):
463464
pytest.fail()
464465
# CWD is not a temporary dir, so no deleted after using it.
465466
assert os.path.isdir(directory)
467+
468+
469+
def test_github_pull_request_head_sha():
470+
fake_event_data = {"pull_request": {"head": {"sha": "headCommitSha"}}}
471+
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as event_file:
472+
json.dump(fake_event_data, event_file)
473+
event_file_path = event_file.name
474+
475+
env = {
476+
"GITHUB_JOB": "test_job",
477+
"GITHUB_EVENT_PATH": event_file_path,
478+
"GITHUB_SERVER_URL": "https://github.com",
479+
"GITHUB_REPOSITORY": "DataDog/dd-trace-py",
480+
"GITHUB_REF": "refs/heads/main",
481+
"GITHUB_HEAD_REF": "main",
482+
"GITHUB_SHA": "mainCommitSha",
483+
"GITHUB_RUN_ID": "12345",
484+
"GITHUB_RUN_NUMBER": "1",
485+
"GITHUB_WORKFLOW": "CI",
486+
"GITHUB_WORKSPACE": "/workspace",
487+
}
488+
489+
tags = ci.extract_github_actions(env=env)
490+
491+
os.remove(event_file_path)
492+
493+
assert tags[git.BRANCH] == "main"
494+
assert tags[git.COMMIT_SHA] == "mainCommitSha"
495+
assert tags[git.REPOSITORY_URL] == "https://github.com/DataDog/dd-trace-py.git"
496+
assert tags[git.COMMIT_HEAD_SHA] == "headCommitSha"
497+
assert tags[ci.JOB_URL] == "https://github.com/DataDog/dd-trace-py/commit/mainCommitSha/checks"
498+
assert tags[ci.PIPELINE_ID] == "12345"
499+
assert tags[ci.PIPELINE_NAME] == "CI"
500+
assert tags[ci.PIPELINE_NUMBER] == "1"
501+
assert tags[ci.PIPELINE_URL] == "https://github.com/DataDog/dd-trace-py/actions/runs/12345"
502+
assert tags[ci.JOB_NAME] == "test_job"
503+
assert tags[ci.PROVIDER_NAME] == "github"
504+
assert tags[ci.WORKSPACE_PATH] == "/workspace"
505+
assert (
506+
tags[ci._CI_ENV_VARS]
507+
== '{"GITHUB_SERVER_URL":"https://github.com","GITHUB_REPOSITORY":"DataDog/dd-trace-py","GITHUB_RUN_ID":"12345"}'
508+
)
509+
510+
511+
def test_extract_git_head_metadata():
512+
fake_user_info = {
513+
"author": ("Author", "[email protected]", "date1"),
514+
"committer": ("Committer", "[email protected]", "date2"),
515+
}
516+
517+
with mock.patch(
518+
"ddtrace.ext.git._is_shallow_repository_with_details", return_value=(True, 0.1, 0)
519+
) as mock_is_shallow, mock.patch("ddtrace.ext.git._unshallow_repository") as mock_unshallow_repository, mock.patch(
520+
"ddtrace.ext.git.extract_user_info", return_value=fake_user_info
521+
), mock.patch(
522+
"ddtrace.ext.git._git_subprocess_cmd", return_value="commit message"
523+
) as mock_git_subprocess_cmd:
524+
tags = git.extract_git_head_metadata("sha123", cwd="/repo")
525+
526+
mock_is_shallow.assert_called_once()
527+
mock_unshallow_repository.assert_called_once()
528+
mock_git_subprocess_cmd.assert_called_once_with("log -n 1 --format=%B sha123", "/repo")
529+
530+
assert tags[git.COMMIT_HEAD_AUTHOR_NAME] == "Author"
531+
assert tags[git.COMMIT_HEAD_AUTHOR_EMAIL] == "[email protected]"
532+
assert tags[git.COMMIT_HEAD_AUTHOR_DATE] == "date1"
533+
assert tags[git.COMMIT_HEAD_COMMITTER_NAME] == "Committer"
534+
assert tags[git.COMMIT_HEAD_COMMITTER_EMAIL] == "[email protected]"
535+
assert tags[git.COMMIT_HEAD_COMMITTER_DATE] == "date2"
536+
assert tags[git.COMMIT_HEAD_MESSAGE] == "commit message"

0 commit comments

Comments
 (0)