Store a repo state file in the log directory

This adds a file named workspace-repos.json to the workspace build
log directory.  It contains the repo state and git commands needed
to reconstruct the repo state used for the job.

A later change may add a tool to translate the structured data in
this file to a shell script.

Other uses of the file may involve reading the repo state directly
to determine the last "real" commit in the tree before Zuul started
creating its speculative vision of the future.

Change-Id: Ifc5ee03fdc41a82f481ac57b8992ba7bd5f9b6c0
Co-Authored-By: Tobias Henkel <tobias.henkel@bmw.de>
This commit is contained in:
James E. Blair 2024-03-21 16:54:12 -07:00
parent c2dc705147
commit 598c362db8
2 changed files with 76 additions and 0 deletions

View File

@ -490,6 +490,50 @@ class TestExecutorRepos(ZuulTestCase, ExecutorReposMixin):
]
self.assertBuildStates(states, projects)
def test_repo_state_file(self):
self.executor_server.hold_jobs_in_build = True
A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
A.addApproval('Code-Review', 2)
self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
self.waitUntilSettled()
build = self.getBuildByName('project-merge')
path = os.path.join(build.jobdir.log_root, 'workspace-repos.json')
with open(path, 'r') as f:
data = json.load(f)
self.executor_server.hold_jobs_in_build = False
self.executor_server.release()
self.waitUntilSettled()
# Don't check the timestamp, but verify it exists
self.assertIsNotNone(data['merge_ops'][2].pop('timestamp', None))
self.assertEqual(
data['merge_ops'],
[{'cmd': ['git', 'checkout', 'master'],
'path': 'review.example.com/org/project1'},
{'cmd': ['git', 'fetch', 'origin', 'refs/changes/01/1/1'],
'path': 'review.example.com/org/project1'},
{'cmd': ['git',
'merge',
'-m',
"Merge 'refs/changes/01/1/1'",
'-s',
'resolve',
'FETCH_HEAD'],
'path': 'review.example.com/org/project1'},
{'cmd': ['git', 'checkout', 'master'],
'path': 'review.example.com/org/project1'}])
self.assertEqual(
set(data['repo_state']['review.example.com/org/project1'].keys()),
{'refs/heads/master', 'refs/remotes/origin/master',
'refs/tags/init'})
self.assertEqual(
"zuul@example.com",
data['merge_email'])
self.assertEqual(
"zuul",
data['merge_name'])
class TestExecutorRepoRoles(ZuulTestCase, ExecutorReposMixin):
tenant_config_file = 'config/executor-repos/main.yaml'

View File

@ -1570,6 +1570,7 @@ class AnsibleJob(object):
zuul_resources = self.prepareNodes(args) # set self.host_list
self.prepareVars(args, zuul_resources) # set self.original_hostvars
self.writeDebugInventory()
self.writeRepoStateFile(repos)
# Early abort if abort requested
if self.aborted:
@ -1612,6 +1613,37 @@ class AnsibleJob(object):
self.log.debug("Sending result: %s", result_data)
self.executor_server.completeBuild(self.build_request, result_data)
def writeRepoStateFile(self, repos):
# Write out the git operation performed up to this point
repo_state_file = os.path.join(self.jobdir.log_root,
'workspace-repos.json')
# First make a connection+project_name -> path mapping
workspace_paths = {}
for project in self.arguments['projects']:
repo = repos.get(project['canonical_name'])
if repo:
workspace_paths[(project['connection'], project['name'])] =\
repo.workspace_project_path
repo_state = {}
for connection_name, connection_value in self.repo_state.items():
for project_name, project_value in connection_value.items():
# We may have data in self.repo_state for repos that
# are not present in the work dir (ie, they are used
# for roles). To keep things simple for now, we will
# omit that, but we could add them later.
workspace_project_path = workspace_paths.get(
(connection_name, project_name))
if workspace_project_path:
repo_state[workspace_project_path] = project_value
repo_state_data = dict(
repo_state=repo_state,
merge_ops=[o.toDict() for o in self.merge_ops],
merge_name=self.executor_server.merge_name,
merge_email=self.executor_server.merge_email,
)
with open(repo_state_file, 'w') as f:
json.dump(repo_state_data, f, sort_keys=True, indent=2)
def getResultData(self):
data = {}
secret_data = {}