-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathimplementation.py
More file actions
302 lines (257 loc) · 11.6 KB
/
Copy pathimplementation.py
File metadata and controls
302 lines (257 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
"""Webhook implementation for Github"""
import boto3
import concurrent.futures
import logging
import os
import random
import shutil
import subprocess
import time
from ci_action.library import aws_client
from ci_action.library import cmake_rewrite
from ci_action.library import github_client
from ci_action.library import pr_resolve
import pprint
BUILD_ENVIRONMENTS = ['gcc', 'intel', 'gcc11']
LOG = logging.getLogger("implementation")
TEST_JOB_TIMEOUT_HOURS = 6
class TimeCheckpointer:
def __init__(self):
self._checkpoint_time = time.time()
def checkpoint(self):
"""Get elapsed time in seconds."""
time_now = time.time()
checkpoint_delta = round(time_now - self._checkpoint_time, 4)
self._checkpoint_time = time_now
return f'<time elapsed: {checkpoint_delta} seconds>'
def check_output(args, **kwargs):
"""
Wrapper around subprocess.check_output that logs the command and its output.
"""
LOG.info(f"Running command: {' '.join(args)}")
return subprocess.check_output(args, **kwargs)
def upload_to_aws(bucket_name, s3_client, tarball_path, s3_file):
"""Upload file to S3 bucket"""
with open(tarball_path, 'rb') as f:
s3_client.put_object(Body=f, Bucket=bucket_name, Key=s3_file)
s3_path = f's3://{bucket_name}/{s3_file}'
return s3_path
def cancel_prior_jobs_and_check_runs(
non_blocking_errors,
infra_config,
config,
):
"""Cancel prior unfinished jobs and check runs for the PR."""
# Use a thread pool to cancel prior unfinished jobs and their associated check runs.
# This process is done in parallel to save time on slow network-bound operations.
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
# Submit operation: cancel prior unfinished AWS Batch jobs for the PR.
cxl_batch_future = executor.submit(
aws_client.cancel_prior_batch_jobs,
job_queue=infra_config['batch_queue'],
repo_name=config['repo_name'],
pr=config["pull_request_number"],
)
# Submit operation: cancel unfinished check runs for the PR.
cxl_checkrun_future = executor.submit(
github_client.cancel_prior_unfinished_check_runs,
repo=config['repo_name'],
owner=config['owner'],
pr_number=config["pull_request_number"],
)
# Wait for the cancel operations to complete.
for future in concurrent.futures.as_completed([cxl_batch_future, cxl_checkrun_future]):
try:
future.result()
except Exception as e:
if future is cxl_batch_future:
non_blocking_errors.append(f"Error cancelling prior batch jobs: {e}")
else:
non_blocking_errors.append(f"Error cancelling prior check runs: {e}")
return non_blocking_errors
def prepare_and_launch_ci_test(
infra_config,
config,
bundle_repo_path,
):
"""The main function that will be called to prepare and launch the CI test.
This is similar to the process_event function, which was used by the lambda
CI actuator but has been adapted for the Github-based Action CI.
Args:
infra_config: The infrastructure configuration for the CI test, pulled
from the cloud formation application resources.
config: The GitHub action environment configuration including
PR metadata and passed config variables.
bundle_repo_path: The path to the bundle repository.
Returns:
A 2-tuple of lists of strings representing errors:
- blocking_errors: Errors that preventing the tests from launching.
- non_blocking_errors Any potentially recoverable errors that may
have occurred during the test launch but did not prevent the test
jobs from launching. These errors will be logged as non-blocking
"""
# Some cleanup and housekeeping operations should not block the test launch
# but should be logged as non-blocking errors so the action can fail (notifying
# us of an issue).
non_blocking_errors = []
# Errors that prevent the tests from launching (e.g. misconfigured test annotations
# or pr groups that don't exist).
blocking_errors = []
timer = TimeCheckpointer() # Timer for logging.
is_scheduled = config.get('is_scheduled', False)
repo_uri = f'https://github.com/{config["owner"]}/{config["repo_name"]}'
# test_annotations analysis for pull requests.
if config["pull_request_number"] and not is_scheduled:
try:
test_annotations = pr_resolve.read_test_annotations(
repo_uri=f'{repo_uri}.git',
pr_number=config['pull_request_number'],
pr_payload=config['pr_payload'],
testmode=config['self_test'],
)
except pr_resolve.Exception as e:
blocking_errors.append(f"Error reading test annotations: {e}")
return blocking_errors, non_blocking_errors
LOG.info('test_annotations:')
annotations_pretty = pprint.pformat(test_annotations._asdict())
LOG.info(f'{timer.checkpoint()}\n{annotations_pretty}')
# Check draft PR run status.
if config.get('pr_payload', {}).get('draft') and not test_annotations.run_on_draft:
LOG.info('\n\nTests are not launched for draft PRs by default.\n'
'To enable testing on draft PRs, add the following annotation to the PR:\n'
'```\n'
'run-ci-on-draft = true\n'
'```\n')
return blocking_errors, non_blocking_errors
# Scheduled or triggered tests have no annotations and rely on default values set below.
else:
test_annotations = pr_resolve.TestAnnotations(
build_group_map={}, # No build group for nightly runs.
debug_mode=False,
next_ci_suffix='', # No suffix, use primary build environment.
test_select='random',
jedi_bundle_branch=None, # If set this overrides the action config.
)
LOG.info(f'{timer.checkpoint()}\nNightly run — using default test annotations.')
bundle_branch = config['bundle_branch'] # This is the default branch to use for the bundle.
if test_annotations.jedi_bundle_branch:
bundle_branch = test_annotations.jedi_bundle_branch # Override based on PR annotations.
# git clone the bundle repository into `bundle_repo_path`
if not os.path.exists(bundle_repo_path):
LOG.info(f"Cloning \"{config['bundle_repository']}@{bundle_branch}\"")
check_output([
'git', 'clone', '--branch', bundle_branch,
config['bundle_repository'], bundle_repo_path
])
repo_to_commit_hash = pr_resolve.gather_build_group_hashes(
test_annotations.build_group_map
)
repo_to_commit_hash_pretty = pprint.pformat(repo_to_commit_hash)
LOG.info(
f'{timer.checkpoint()}\nrepo_to_commit_hash:\n{repo_to_commit_hash_pretty}'
)
# Import the bundle file
bundle_file = os.path.join(bundle_repo_path, 'CMakeLists.txt')
bundle_original = os.path.join(
bundle_repo_path, 'CMakeLists.txt.original'
)
with open(bundle_file, 'r') as f:
bundle = cmake_rewrite.CMakeFile(f.read())
# Move the original bundle file to the original file.
shutil.move(bundle_file, bundle_original)
# Rewrite the bundle cmake file exchanging branch references for
# commit hashes from the build group.
with open(bundle_file, 'w') as f:
bundle.rewrite_from_build_groups(
file_object=f,
build_group_commit_map=repo_to_commit_hash,
)
LOG.info(f'{timer.checkpoint()}\n Wrote CMakeLists.')
# Add resources to the bundle by copying all files in /app/shell to jedi_ci_resources
shutil.copytree(
'/app/shell', os.path.join(bundle_repo_path, 'jedi_ci_resources')
)
# Create a tarball the new bundle (with test resources).
LOG.info(f"Creating bundle.tar.gz from {bundle_repo_path}")
bundle_tarball = "bundle.tar.gz"
check_output([
'tar', '-czf', bundle_tarball, '-C', os.path.dirname(bundle_repo_path),
os.path.basename(bundle_repo_path)
])
LOG.info(f"{timer.checkpoint()}\nCreated bundle tarball at {bundle_tarball}")
# Upload the bundle to S3.
s3_file = (
f'ci_action_bundles/{config["repository"]}/'
f'{config["build_id_name"]}-bundle.tar.gz'
)
s3_client = boto3.client('s3')
configured_bundle_tarball_s3_path = upload_to_aws(
config['build_cache_bucket'], s3_client, bundle_tarball, s3_file
)
# Select the build environments to test.
test_select = test_annotations.test_select
if test_select == 'random':
chosen_build_environments = [random.choice(BUILD_ENVIRONMENTS)]
elif test_select == 'all':
chosen_build_environments = [e for e in BUILD_ENVIRONMENTS]
else:
chosen_build_environments = [test_select]
if not is_scheduled:
non_blocking_errors = cancel_prior_jobs_and_check_runs(
non_blocking_errors,
infra_config,
config,
)
# This is a constructor for the configuration needed to submit AWS Batch jobs.
# This constructor reads configuration from the environment and must be
# configured via environmental variables set in the Lambda function. Note that
# the timeout is set to is 4 hours since even a full cache rebuild should much
# less time. For information on the config variables.
batch_config_builder = aws_client.BatchSubmitConfigBuilder(
job_name_map=infra_config['batch_job_name_map'],
job_queue=infra_config['batch_queue'],
timeout=60 * 60 * TEST_JOB_TIMEOUT_HOURS,
)
# write the test github check runs to the PR.
for build_environment in chosen_build_environments:
# Create a single GitHub check run for this build environment.
check_run_id = github_client.create_check_run(
github_client.JEDI_CI_PREFIX,
build_environment,
config['repo_name'],
config['owner'],
config['trigger_commit'],
test_annotations.next_ci_suffix)
LOG.info(f'{timer.checkpoint()}\nCreated check run for build_environment '
f'{build_environment}: {repo_uri}/runs/{check_run_id}')
debug_time = 60 * 30 if test_annotations.debug_mode else 0
build_identity = (
f'{config["repo_name"]}-'
f'{config["build_id_name"]}-{build_environment}'
)
repo_name_full = (
f'{config["owner"]}/{config["repo_name"]}'
)
job = aws_client.submit_test_batch_job(
config=batch_config_builder.get_config(
build_environment + test_annotations.next_ci_suffix
),
repo_name=config['repo_name'],
repo_name_full=repo_name_full,
build_id=config['build_id_name'],
configured_bundle_tarball=configured_bundle_tarball_s3_path,
debug_time_seconds=debug_time,
build_identity=build_identity,
unittest_tag=config['unittest_tag'],
trigger_sha=config['trigger_commit'],
trigger_pr=str(config['pull_request_number']),
check_run_id=check_run_id,
test_script=config['test_script'],
is_scheduled=is_scheduled,
)
job_arn = job['jobArn']
LOG.info(
f'{timer.checkpoint()}\nSubmitted Batch Job for build environment '
f'{build_environment}: "{job_arn}".'
)
return blocking_errors, non_blocking_errors