Fix deployment preparation recovery and unify GitHub authentication
Development CI / check-and-build (push) Successful in 33s
Development CI / deploy-development (push) Successful in 2m27s
Main CI / check-and-build (push) Successful in 27s
GitHub Git mirror / mirror (push) Successful in 5s

This commit is contained in:
eezd committed 2026-10-30 11:33:53 +08:00
1 parent b4f29d4966
commit 549b818e95
5 files changed
+124 -25

No files matched your search

+79
View File
@@ -81,6 +81,85 @@ class BuildTests(unittest.TestCase):
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
class RecoveryTests(unittest.TestCase):
def test_pending_write_failure_never_pauses_trader(self):
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
previous = {'images': {part: {'digest': 'synthetic'} for part in ci.IMAGES}}
ci.save(directory / 'deployment.json', previous)
(directory / 'compose.yml').write_text('synthetic')
running = [{'Config': {'Labels': {'com.docker.compose.service': 'trader'}}, 'State': {'Running': True}}]
with patch.object(deploy, 'INSTANCE', directory), patch.object(deploy, 'branch_head', return_value='a' * 40), \
patch.object(deploy, 'validate_images'), patch.object(deploy, 'inspect_services', return_value=running), \
patch.object(deploy, 'run', return_value='{"busy":0}') as run, \
patch.object(deploy, 'save', side_effect=OSError('disk failure')):
with self.assertRaises(OSError):
deploy.deploy({**previous, 'commit': 'a' * 40}, directory)
self.assertFalse(any(call.args[:2] == ('docker', 'pause') for call in run.call_args_list))
def test_busy_recheck_recovers_without_stopping_services(self):
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
previous = {'images': {part: {'digest': 'synthetic'} for part in ci.IMAGES}}
ci.save(directory / 'deployment.json', previous)
(directory / 'compose.yml').write_text('synthetic')
running = [{'Config': {'Labels': {'com.docker.compose.service': 'trader'}}, 'State': {'Running': True}}]
idle = iter(['{"busy":0}', '{"busy":1}'])
def command(*args, **kwargs):
if kwargs.get('input') == deploy.IDLE:
return next(idle)
if args[:2] == ('docker', 'inspect'):
return '[{"State":{"Paused":true}}]'
if args[:2] == ('docker', 'ps'):
return 'trader-id'
return '{}'
with patch.object(deploy, 'INSTANCE', directory), patch.object(deploy, 'branch_head', return_value='a' * 40), \
patch.object(deploy, 'validate_images'), patch.object(deploy, 'inspect_services', return_value=running), \
patch.object(deploy, 'run', side_effect=command) as run:
with self.assertRaisesRegex(RuntimeError, 'Analysis became active'):
deploy.deploy({**previous, 'commit': 'a' * 40}, directory)
self.assertFalse(any('stop' in call.args for call in run.call_args_list))
self.assertTrue(any(call.args[:2] == ('docker', 'unpause') for call in run.call_args_list))
self.assertFalse((directory / 'deployment.pending.json').exists())
def test_preparation_recovery_unpauses_without_replacing_services(self):
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
ci.save(directory / 'deployment.pending.json', {})
with patch.object(deploy, 'run', side_effect=['trader-id', '[{"State":{"Paused":true}}]', '']) as run, \
patch.object(deploy, 'validate_images') as validate:
deploy.restore(directory, {'previous': {}, 'phase': 'preparing'})
run.assert_called_with('docker', 'unpause', 'trader-id')
validate.assert_not_called()
self.assertFalse((directory / 'deployment.pending.json').exists())
def test_failed_unpause_preserves_pending(self):
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
ci.save(directory / 'deployment.pending.json', {})
with patch.object(deploy, 'run', side_effect=['trader-id', '[{"State":{"Paused":true}}]', RuntimeError('unpause failed')]):
with self.assertRaisesRegex(RuntimeError, 'unpause failed'):
deploy.restore(directory, {'previous': {}, 'phase': 'preparing'})
self.assertTrue((directory / 'deployment.pending.json').exists())
def test_missing_or_wrong_github_identity_never_synchronizes(self):
for token, identity in [('', None), ('synthetic', {'login': 'other'})]:
with self.subTest(token=bool(token)), patch.dict(os.environ, {'ALL_GITHUB_TOKEN': token}), \
patch.object(sys, 'argv', ['mirror.py']), patch.object(mirror, 'request_json', return_value=identity), \
patch.object(mirror, 'synchronize') as sync:
with self.assertRaises(RuntimeError):
mirror.main()
sync.assert_not_called()
def test_github_read_only_token_never_synchronizes(self):
with patch.dict(os.environ, {'ALL_GITHUB_TOKEN': 'synthetic'}), patch.object(sys, 'argv', ['mirror.py']), \
patch.object(mirror, 'request_json', side_effect=[{'login': 'eezd'}, {'permissions': {'push': False}}]), \
patch.object(mirror, 'synchronize') as sync:
with self.assertRaisesRegex(RuntimeError, 'write permission'):
mirror.main()
sync.assert_not_called()
class MirrorTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()