Skip to content

Commit 70032cf

Browse files
authored
CI-App:Read metadata from local git repository (#1561)
1 parent 707d41f commit 70032cf

File tree

20 files changed

+248
-9
lines changed

20 files changed

+248
-9
lines changed

.dd-ci/ci-app-spec.json

+4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
"git.commit.sha",
1818
"git.branch",
1919
"git.tag",
20+
"git.commit.author.date",
2021
"git.commit.author.email",
2122
"git.commit.author.name",
23+
"git.commit.committer.date",
24+
"git.commit.committer.email",
25+
"git.commit.committer.name",
2226
"git.commit.message"
2327
]

integration/apps/rspec/bin/setup

+6
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@ chdir APP_ROOT do
1414
puts "\n== Installing dependencies =="
1515
system! 'gem install bundler --conservative'
1616
system('bundle check') || system!('bundle install')
17+
18+
# Create an non-trivial git repository, to augment test payload information.
19+
system! 'git init'
20+
system! 'GIT_AUTHOR_NAME="Test Author" GIT_AUTHOR_EMAIL=author@local.test ' \
21+
'GIT_COMMITTER_NAME="Test Committer" GIT_COMMITTER_EMAIL=committer@local.test ' \
22+
'git commit --allow-empty -m "Test message"'
1723
end

integration/apps/rspec/docker-compose.yml

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ services:
2727
tty: true
2828
volumes:
2929
- .:/app
30+
- /app/.git
3031
- ./data/app:/data/app
3132
- bundle:/usr/local/bundle
3233
- ../../images/include:/vendor/dd-demo

lib/datadog/ci/ext/environment.rb

+110-1
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@
22

33
require 'ddtrace/ext/git'
44

5+
require 'open3'
6+
57
module Datadog
68
module CI
79
module Ext
810
# Defines constants for CI tags
911
# rubocop:disable Metrics/ModuleLength:
1012
module Environment
11-
TAG_STAGE_NAME = 'ci.stage.name'
1213
TAG_JOB_NAME = 'ci.job.name'
1314
TAG_JOB_URL = 'ci.job.url'
1415
TAG_PIPELINE_ID = 'ci.pipeline.id'
1516
TAG_PIPELINE_NAME = 'ci.pipeline.name'
1617
TAG_PIPELINE_NUMBER = 'ci.pipeline.number'
1718
TAG_PIPELINE_URL = 'ci.pipeline.url'
1819
TAG_PROVIDER_NAME = 'ci.provider.name'
20+
TAG_STAGE_NAME = 'ci.stage.name'
1921
TAG_WORKSPACE_PATH = 'ci.workspace_path'
2022

2123
PROVIDERS = [
@@ -53,6 +55,11 @@ def tags(env)
5355
tags = {}
5456
end
5557

58+
# Fill out tags from local git as fallback
59+
extract_local_git.each do |key, value|
60+
tags[key] ||= value
61+
end
62+
5663
tags.reject { |_, v| v.nil? }
5764
end
5865

@@ -312,6 +319,108 @@ def extract_bitrise(env)
312319
}
313320
end
314321

322+
def git_commit_users
323+
# Get committer and author information in one command.
324+
output = exec_git_command("git show -s --format='%an\t%ae\t%at\t%cn\t%ce\t%ct'")
325+
return unless output
326+
327+
fields = output.split("\t").each(&:strip!)
328+
329+
{
330+
author_name: fields[0],
331+
author_email: fields[1],
332+
# Because we can't get a reliable UTC time from all recent versions of git
333+
# We have to rely on converting the date to UTC ourselves.
334+
author_date: Time.at(fields[2].to_i).utc.to_datetime.iso8601,
335+
committer_name: fields[3],
336+
committer_email: fields[4],
337+
# Because we can't get a reliable UTC time from all recent versions of git
338+
# We have to rely on converting the date to UTC ourselves.
339+
committer_date: Time.at(fields[5].to_i).utc.to_datetime.iso8601
340+
}
341+
rescue => e
342+
Datadog.logger.debug("Unable to read git commit users: #{e.message} at #{e.backtrace.first}")
343+
nil
344+
end
345+
346+
def git_repository_url
347+
exec_git_command('git ls-remote --get-url')
348+
rescue => e
349+
Datadog.logger.debug("Unable to read git repository url: #{e.message} at #{e.backtrace.first}")
350+
nil
351+
end
352+
353+
def git_commit_message
354+
exec_git_command('git show -s --format=%s')
355+
rescue => e
356+
Datadog.logger.debug("Unable to read git commit message: #{e.message} at #{e.backtrace.first}")
357+
nil
358+
end
359+
360+
def git_branch
361+
exec_git_command('git rev-parse --abbrev-ref HEAD')
362+
rescue => e
363+
Datadog.logger.debug("Unable to read git branch: #{e.message} at #{e.backtrace.first}")
364+
nil
365+
end
366+
367+
def git_commit_sha
368+
exec_git_command('git rev-parse HEAD')
369+
rescue => e
370+
Datadog.logger.debug("Unable to read git commit SHA: #{e.message} at #{e.backtrace.first}")
371+
nil
372+
end
373+
374+
def git_tag
375+
exec_git_command('git tag --points-at HEAD')
376+
rescue => e
377+
Datadog.logger.debug("Unable to read git tag: #{e.message} at #{e.backtrace.first}")
378+
nil
379+
end
380+
381+
def git_base_directory
382+
exec_git_command('git rev-parse --show-toplevel')
383+
rescue => e
384+
Datadog.logger.debug("Unable to read git base directory: #{e.message} at #{e.backtrace.first}")
385+
nil
386+
end
387+
388+
def exec_git_command(cmd)
389+
out, status = Open3.capture2e(cmd)
390+
391+
raise "Failed to run git command #{cmd}: #{out}" unless status.success?
392+
393+
out.strip! # There's always a "\n" at the end of the command output
394+
395+
return nil if out.empty?
396+
397+
out
398+
end
399+
400+
def extract_local_git
401+
env = {
402+
TAG_WORKSPACE_PATH => git_base_directory,
403+
Datadog::Ext::Git::TAG_REPOSITORY_URL => git_repository_url,
404+
Datadog::Ext::Git::TAG_COMMIT_SHA => git_commit_sha,
405+
Datadog::Ext::Git::TAG_BRANCH => git_branch,
406+
Datadog::Ext::Git::TAG_TAG => git_tag,
407+
Datadog::Ext::Git::TAG_COMMIT_MESSAGE => git_commit_message
408+
}
409+
410+
if (commit_users = git_commit_users)
411+
env.merge!(
412+
Datadog::Ext::Git::TAG_COMMIT_AUTHOR_NAME => commit_users[:author_name],
413+
Datadog::Ext::Git::TAG_COMMIT_AUTHOR_EMAIL => commit_users[:author_email],
414+
Datadog::Ext::Git::TAG_COMMIT_AUTHOR_DATE => commit_users[:author_date],
415+
Datadog::Ext::Git::TAG_COMMIT_COMMITTER_NAME => commit_users[:committer_name],
416+
Datadog::Ext::Git::TAG_COMMIT_COMMITTER_EMAIL => commit_users[:committer_email],
417+
Datadog::Ext::Git::TAG_COMMIT_COMMITTER_DATE => commit_users[:committer_date]
418+
)
419+
end
420+
421+
env
422+
end
423+
315424
def branch_or_tag(branch_or_tag)
316425
branch = tag = nil
317426
if branch_or_tag.include?('tags/')

lib/ddtrace/ext/git.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ module Git
66
TAG_REPOSITORY_URL = 'git.repository_url'.freeze
77
TAG_TAG = 'git.tag'.freeze
88

9-
TAG_COMMIT_AUTHOR_DATE = 'git.commit.author.date'.freeze # DEV: Not populated yet
9+
TAG_COMMIT_AUTHOR_DATE = 'git.commit.author.date'.freeze
1010
TAG_COMMIT_AUTHOR_EMAIL = 'git.commit.author.email'.freeze
1111
TAG_COMMIT_AUTHOR_NAME = 'git.commit.author.name'.freeze
12-
TAG_COMMIT_COMMITTER_DATE = 'git.commit.committer.date'.freeze # DEV: Not populated yet
13-
TAG_COMMIT_COMMITTER_EMAIL = 'git.commit.committer.email'.freeze # DEV: Not populated yet
14-
TAG_COMMIT_COMMITTER_NAME = 'git.commit.committer.name'.freeze # DEV: Not populated yet
12+
TAG_COMMIT_COMMITTER_DATE = 'git.commit.committer.date'.freeze
13+
TAG_COMMIT_COMMITTER_EMAIL = 'git.commit.committer.email'.freeze
14+
TAG_COMMIT_COMMITTER_NAME = 'git.commit.committer.name'.freeze
1515
TAG_COMMIT_MESSAGE = 'git.commit.message'.freeze
1616
TAG_COMMIT_SHA = 'git.commit.sha'.freeze
1717
end

spec/datadog/ci/ext/environment_spec.rb

+98-4
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,64 @@
44
require 'datadog/ci/ext/environment'
55

66
RSpec.describe Datadog::CI::Ext::Environment do
7+
FIXTURE_DIR = "#{File.dirname(__FILE__)}/fixtures/" # rubocop:disable all
8+
79
describe '.tags' do
8-
subject(:tags) { described_class.tags(env) }
10+
subject(:tags) do
11+
ClimateControl.modify(environment_variables) { described_class.tags(env) }
12+
end
13+
14+
let(:env) { {} }
15+
let(:environment_variables) { {} }
16+
17+
shared_context 'with git fixture' do |git_fixture|
18+
let(:environment_variables) do
19+
super().merge('GIT_DIR' => "#{FIXTURE_DIR}/git/#{git_fixture}", 'GIT_WORK_TREE' => "#{FIXTURE_DIR}/git/")
20+
end
21+
end
22+
23+
shared_context 'without git installed' do
24+
before { allow(Open3).to receive(:capture2e).and_raise(Errno::ENOENT, 'No such file or directory - git') }
25+
end
926

10-
Dir.glob("#{File.dirname(__FILE__)}/fixtures/ci/*.json") do |filename|
27+
Dir.glob("#{FIXTURE_DIR}/ci/*.json").sort.each do |filename|
28+
# Parse each CI provider file
1129
File.open(filename) do |f|
1230
context "for fixture #{File.basename(filename)}" do
31+
# Create a context for each example inside the JSON fixture file
1332
JSON.parse(f.read).each_with_index do |(env, tags), i|
1433
context "##{i}" do
34+
# Modify HOME so that '~' expansion matches CI home directory.
35+
let(:environment_variables) { super().merge('HOME' => env['HOME']) }
1536
let(:env) { env }
1637

17-
it 'matches tags' do
18-
ClimateControl.modify('HOME' => env['HOME']) do
38+
context 'with git information' do
39+
include_context 'with git fixture', 'gitdir_with_commit'
40+
41+
it 'matches CI tags, with git fallback information' do
42+
is_expected
43+
.to eq(
44+
{
45+
'ci.workspace_path' => "#{Dir.pwd}/spec/datadog/ci/ext/fixtures/git",
46+
'git.branch' => 'master',
47+
'git.commit.author.date' => '2011-02-16T13:00:00+00:00',
48+
'git.commit.author.email' => 'bot@friendly.test',
49+
'git.commit.author.name' => 'Friendly bot',
50+
'git.commit.committer.date' => '2021-06-17T18:35:10+00:00',
51+
'git.commit.committer.email' => 'marco.costa@datadoghq.com',
52+
'git.commit.committer.name' => 'Marco Costa',
53+
'git.commit.message' => 'First commit!',
54+
'git.commit.sha' => '9322ca1d57975b49b8c00b449d21b06660ce8b5b',
55+
'git.repository_url' => 'https://datadoghq.com/git/test.git'
56+
}.merge(tags)
57+
)
58+
end
59+
end
60+
61+
context 'without git information' do
62+
include_context 'without git installed'
63+
64+
it 'matches only CI tags' do
1965
is_expected.to eq(tags)
2066
end
2167
end
@@ -24,5 +70,53 @@
2470
end
2571
end
2672
end
73+
74+
context 'inside a git directory' do
75+
context 'with a newly created git repository' do
76+
include_context 'with git fixture', 'gitdir_empty'
77+
it 'matches tags' do
78+
is_expected.to eq('ci.workspace_path' => "#{Dir.pwd}/spec/datadog/ci/ext/fixtures/git")
79+
end
80+
end
81+
82+
context 'with a git repository with a commit' do
83+
include_context 'with git fixture', 'gitdir_with_commit'
84+
it 'matches tags' do
85+
is_expected.to eq(
86+
'ci.workspace_path' => "#{Dir.pwd}/spec/datadog/ci/ext/fixtures/git",
87+
'git.branch' => 'master',
88+
'git.commit.author.date' => '2011-02-16T13:00:00+00:00',
89+
'git.commit.author.email' => 'bot@friendly.test',
90+
'git.commit.author.name' => 'Friendly bot',
91+
'git.commit.committer.date' => '2021-06-17T18:35:10+00:00',
92+
'git.commit.committer.email' => 'marco.costa@datadoghq.com',
93+
'git.commit.committer.name' => 'Marco Costa',
94+
'git.commit.message' => 'First commit!',
95+
'git.commit.sha' => '9322ca1d57975b49b8c00b449d21b06660ce8b5b',
96+
'git.repository_url' => 'https://datadoghq.com/git/test.git'
97+
)
98+
end
99+
end
100+
101+
context 'not inside a git repository' do
102+
let(:environment_variables) { { 'GIT_DIR' => './tmp/not-a-git-dir' } }
103+
104+
it 'does not fail' do
105+
is_expected.to eq({})
106+
end
107+
end
108+
109+
context 'without git installed nor CI information' do
110+
include_context 'without git installed'
111+
112+
it 'does not fail' do
113+
allow(Datadog.logger).to receive(:debug)
114+
115+
is_expected.to eq({})
116+
117+
expect(Datadog.logger).to have_received(:debug).with(/No such file or directory - git/).at_least(1).time
118+
end
119+
end
120+
end
27121
end
28122
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ref: refs/heads/master
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[core]
2+
repositoryformatversion = 0
3+
filemode = true
4+
bare = false
5+
logallrefupdates = true
6+
ignorecase = true
7+
precomposeunicode = true

spec/datadog/ci/ext/fixtures/git/gitdir_empty/objects/.keep

Whitespace-only changes.

spec/datadog/ci/ext/fixtures/git/gitdir_empty/refs/.keep

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ref: refs/heads/master
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[core]
2+
repositoryformatversion = 0
3+
filemode = true
4+
bare = false
5+
logallrefupdates = true
6+
ignorecase = true
7+
precomposeunicode = true
8+
[remote "origin"]
9+
url = https://datadoghq.com/git/test.git
10+
fetch = +refs/heads/*:refs/remotes/origin/*
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
0000000000000000000000000000000000000000 3e84e4841fc8f3c76e54fb1becaa8863a2cede30 Marco Costa <marco.costa@datadoghq.com> 1623881249 -0700 commit (initial): First commit!
2+
3e84e4841fc8f3c76e54fb1becaa8863a2cede30 9322ca1d57975b49b8c00b449d21b06660ce8b5b Marco Costa <marco.costa@datadoghq.com> 1623954910 -0700 commit (amend): First commit!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
0000000000000000000000000000000000000000 3e84e4841fc8f3c76e54fb1becaa8863a2cede30 Marco Costa <marco.costa@datadoghq.com> 1623881249 -0700 commit (initial): First commit!
2+
3e84e4841fc8f3c76e54fb1becaa8863a2cede30 9322ca1d57975b49b8c00b449d21b06660ce8b5b Marco Costa <marco.costa@datadoghq.com> 1623954910 -0700 commit (amend): First commit!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
9322ca1d57975b49b8c00b449d21b06660ce8b5b
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3e84e4841fc8f3c76e54fb1becaa8863a2cede30

0 commit comments

Comments
 (0)