Skip to content

Commit a7e391c

Browse files
chore(ci): unify Trigger preparation and promotion jobs
1 parent 1d08664 commit a7e391c

2 files changed

Lines changed: 141 additions & 179 deletions

File tree

‎.github/scripts/test-trigger-deploy.py‎

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,10 @@ def setUpClass(cls):
336336
str(workflow)], check=True, capture_output=True, text=True)
337337
cls.jobs = json.loads(parsed.stdout)['jobs']
338338

339-
def eligible(self, job, branch, results, event='push', cancelled=False, promoted='true'):
340-
expression = self.jobs[job]['if']
339+
def evaluate(self, expression, branch, results=None, event='push', cancelled=False, promoted='true'):
340+
results = results or {}
341+
if expression.startswith('${{'):
342+
expression = expression[3:-2]
341343
expression = re.sub(r'needs\.([\w-]+)\.result', lambda m: repr(results[m[1]]), expression)
342344
expression = expression.replace('needs.promote-images.outputs.promoted', repr(promoted))
343345
expression = expression.replace('github.ref', repr('refs/heads/' + branch))
@@ -346,19 +348,21 @@ def eligible(self, job, branch, results, event='push', cancelled=False, promoted
346348
expression = expression.replace('&&', ' and ').replace('||', ' or ')
347349
return eval(' '.join(expression.split()), {'__builtins__': {}})
348350

351+
def eligible(self, job, branch, results, **options):
352+
return self.evaluate(self.jobs[job]['if'], branch, results, **options)
353+
349354
def release_results(self, branch):
350-
active = ('migrate-dev', 'build-dev', 'deploy-trigger-dev') if branch == 'dev' else (
351-
'migrate', 'build-amd64', 'deploy-trigger')
355+
active = ('migrate-dev', 'build-dev', 'prepare-trigger') if branch == 'dev' else (
356+
'migrate', 'build-amd64', 'prepare-trigger')
352357
results = {name: 'success' if name in active else 'skipped'
353358
for name in self.jobs['promote-images']['needs']}
354359
return active, results
355360

356361
def test_uploads_and_image_builds_can_start_before_migration(self):
357-
for job in ('deploy-trigger', 'deploy-trigger-dev', 'build-amd64', 'build-dev'):
362+
for job in ('prepare-trigger', 'build-amd64', 'build-dev'):
358363
self.assertFalse(self.jobs[job].get('needs'), job)
359-
for job in ('deploy-trigger', 'deploy-trigger-dev'):
360-
upload = next(step for step in self.jobs[job]['steps'] if step.get('id') == 'deploy')
361-
self.assertIn('--skip-promotion', upload['run'])
364+
upload = next(step for step in self.jobs['prepare-trigger']['steps'] if step.get('id') == 'deploy')
365+
self.assertIn('--skip-promotion', upload['run'])
362366

363367
def test_each_release_waits_for_all_three_gates(self):
364368
for branch in ('main', 'staging', 'dev'):
@@ -387,9 +391,9 @@ def test_dev_build_cannot_move_deploy_tags(self):
387391
self.assertNotIn('imagetools create', json.dumps(steps))
388392

389393
def test_task_promotion_requires_a_successful_fresh_app_release(self):
390-
for branch, job, upload in (('main', 'promote-trigger', 'deploy-trigger'),
391-
('staging', 'promote-trigger', 'deploy-trigger'),
392-
('dev', 'promote-trigger-dev', 'deploy-trigger-dev')):
394+
for branch, job, upload in (('main', 'promote-trigger', 'prepare-trigger'),
395+
('staging', 'promote-trigger', 'prepare-trigger'),
396+
('dev', 'promote-trigger', 'prepare-trigger')):
393397
ready = {'promote-images': 'success', upload: 'success'}
394398
self.assertIn('promote-images', self.jobs[job]['needs'])
395399
self.assertTrue(self.eligible(job, branch, ready))
@@ -401,6 +405,81 @@ def test_task_promotion_requires_a_successful_fresh_app_release(self):
401405
self.assertLess(wait, promote)
402406
self.assertEqual(steps[promote]['env']['VERSION'], '${{ needs.' + upload + '.outputs.version }}')
403407

408+
def test_shared_trigger_jobs_use_the_same_target_for_upload_and_promotion(self):
409+
self.assertEqual({job for job in self.jobs if 'trigger' in job}, {'prepare-trigger', 'promote-trigger'})
410+
prepare = self.jobs['prepare-trigger']
411+
select = next(step for step in prepare['steps'] if step.get('id') == 'target')
412+
upload = next(step for step in prepare['steps'] if step.get('id') == 'deploy')
413+
promote = next(step for step in self.jobs['promote-trigger']['steps']
414+
if 'promote "$VERSION"' in step.get('run', ''))
415+
for variable, output in (('TRIGGER_ENV', 'environment'), ('TRIGGER_BRANCH', 'preview_branch')):
416+
self.assertEqual(upload['env'][variable], '${{ steps.target.outputs.' + output + ' }}')
417+
self.assertEqual(prepare['outputs'][output], '${{ steps.target.outputs.' + output + ' }}')
418+
self.assertEqual(promote['env'][variable], '${{ needs.prepare-trigger.outputs.' + output + ' }}')
419+
for branch, target, preview_branch in (('main', 'prod', ''), ('staging', 'staging', ''),
420+
('dev', 'preview', 'dev-sim')):
421+
with self.subTest(branch=branch), tempfile.TemporaryDirectory() as directory:
422+
root = Path(directory)
423+
env = {**os.environ, 'GITHUB_REF': 'refs/heads/' + branch,
424+
'GITHUB_OUTPUT': str(root / 'outputs'), 'PATH': f'{root}:{os.environ["PATH"]}'}
425+
subprocess.run(['bash', '-eo', 'pipefail', '-c', select['run']], env=env, check=True)
426+
outputs = dict(line.split('=', 1) for line in (root / 'outputs').read_text().splitlines())
427+
self.assertEqual(outputs, {'environment': target, 'preview_branch': preview_branch})
428+
(root / 'bunx').write_text('''#!/usr/bin/env python3
429+
import json, os, sys
430+
with open(os.environ['CLI_CALLS'], 'a') as stream:
431+
stream.write(json.dumps(sys.argv[1:]) + '\\n')
432+
''')
433+
(root / 'bunx').chmod(0o755)
434+
env.update({'TRIGGER_ENV': outputs['environment'], 'TRIGGER_BRANCH': outputs['preview_branch'],
435+
'TRIGGER_ACCESS_TOKEN': 'test-token', 'TRIGGER_PROJECT_ID': 'test-project',
436+
'VERSION': '20260101.1', 'CLI_CALLS': str(root / 'calls')})
437+
for step in (upload, promote):
438+
subprocess.run(['bash', '-eo', 'pipefail', '-c', step['run']], env=env,
439+
check=True, capture_output=True, text=True)
440+
calls = [json.loads(line) for line in (root / 'calls').read_text().splitlines()]
441+
flags = ['--env', target] + (['--branch', preview_branch] if preview_branch else [])
442+
self.assertEqual(calls, [
443+
['trigger.dev@4.5.12', 'deploy', *flags, '--skip-promotion'],
444+
['trigger.dev@4.5.12', 'promote', '20260101.1', *flags],
445+
])
446+
self.assertTrue(self.eligible('prepare-trigger', branch, {}))
447+
self.assertFalse(self.eligible('prepare-trigger', branch, {}, event='pull_request'))
448+
449+
def test_unsupported_trigger_ref_fails_before_upload(self):
450+
select = next(step for step in self.jobs['prepare-trigger']['steps'] if step.get('id') == 'target')
451+
with tempfile.TemporaryDirectory() as directory:
452+
output = Path(directory) / 'outputs'
453+
result = subprocess.run(['bash', '-eo', 'pipefail', '-c', select['run']],
454+
env={**os.environ, 'GITHUB_REF': 'refs/heads/unsupported',
455+
'GITHUB_OUTPUT': str(output)}, capture_output=True, text=True)
456+
self.assertNotEqual(result.returncode, 0)
457+
self.assertIn('unsupported Trigger release ref', result.stderr)
458+
self.assertFalse(output.exists())
459+
self.assertFalse(self.eligible('prepare-trigger', 'unsupported', {}))
460+
461+
def test_shared_promotion_preserves_environment_session_and_poll_budgets(self):
462+
job = self.jobs['promote-trigger']
463+
credentials = next(step for step in job['steps'] if step['name'] == 'Configure AWS credentials')['with']
464+
wait = next(step for step in job['steps'] if step['name'] == 'Wait for ECS traffic cutover')['env']
465+
for branch, poll, session, minutes, role in (
466+
('main', 4200, 5400, 90, 'prod-role'),
467+
('staging', 4200, 5400, 90, 'staging-role'),
468+
('dev', 1200, 2400, 40, 'dev-role'),
469+
):
470+
with self.subTest(branch=branch):
471+
self.assertEqual(self.evaluate(wait['OVERALL_TIMEOUT'], branch), poll)
472+
self.assertEqual(self.evaluate(credentials['role-duration-seconds'], branch), session)
473+
self.assertEqual(self.evaluate(job['timeout-minutes'], branch), minutes)
474+
role_expression = credentials['role-to-assume']
475+
for key, value in (('AWS_ROLE_TO_ASSUME', 'prod-role'),
476+
('STAGING_AWS_ROLE_TO_ASSUME', 'staging-role'),
477+
('DEV_AWS_ROLE_TO_ASSUME', 'dev-role')):
478+
role_expression = role_expression.replace('secrets.' + key, repr(value))
479+
self.assertEqual(self.evaluate(role_expression, branch), role)
480+
self.assertGreater(session, poll)
481+
self.assertGreater(minutes * 60, poll)
482+
404483
def test_permission_check_and_other_images_precede_app_rollout(self):
405484
steps = self.jobs['promote-images']['steps']
406485
preflight = next(i for i, step in enumerate(steps) if 'get-pipeline-state' in step.get('run', ''))

0 commit comments

Comments
 (0)