ci: add per-image jobs to homelab CI
ci / Compose (push) Skipped
ci / Workflows (push) Skipped
ci / Shell (push) Skipped
ci / Formatting (push) Skipped
ci / Python and tests (push) Skipped
ci / YAML (push) Skipped
ci / Dockerfiles (push) Skipped
ci / Kubernetes (push) Skipped
ci / Compose (pull_request) Successful in 11s
ci / Workflows (pull_request) Successful in 7s
ci / Shell (pull_request) Successful in 15s
ci / Formatting (pull_request) Successful in 15s
ci / Python and tests (pull_request) Successful in 5s
ci / YAML (pull_request) Successful in 7s
ci / Dockerfiles (pull_request) Successful in 5s
ci / Kubernetes (pull_request) Successful in 6s
ci / image-plan (pull_request) Skipped
ci / Image (${{ matrix.name }}) (pull_request) Skipped
ci / build (pull_request) Skipped
renovate-ci / validate-renovate (pull_request) Successful in 11s
ci / Compose (push) Skipped
ci / Workflows (push) Skipped
ci / Shell (push) Skipped
ci / Formatting (push) Skipped
ci / Python and tests (push) Skipped
ci / YAML (push) Skipped
ci / Dockerfiles (push) Skipped
ci / Kubernetes (push) Skipped
ci / Compose (pull_request) Successful in 11s
ci / Workflows (pull_request) Successful in 7s
ci / Shell (pull_request) Successful in 15s
ci / Formatting (pull_request) Successful in 15s
ci / Python and tests (pull_request) Successful in 5s
ci / YAML (pull_request) Successful in 7s
ci / Dockerfiles (pull_request) Successful in 5s
ci / Kubernetes (pull_request) Successful in 6s
ci / image-plan (pull_request) Skipped
ci / Image (${{ matrix.name }}) (pull_request) Skipped
ci / build (pull_request) Skipped
renovate-ci / validate-renovate (pull_request) Successful in 11s
This commit is contained in:
1 parent
1d93588e06
commit
95e4d8f875
4 files changed
+439
-110
No files matched your search
@@ -40,19 +40,6 @@ class ArtifactTests(unittest.TestCase):
|
||||
release_module.render(io.StringIO(f' image: "{image}:prod" # note\n'), result)
|
||||
self.assertEqual(result.getvalue(), f' image: "{image}@sha256:{"b" * 64}" # note\n')
|
||||
|
||||
def test_edu_release_digest_is_preserved(self):
|
||||
with tempfile.TemporaryDirectory() as scratch:
|
||||
path = Path(scratch) / 'release.json'
|
||||
path.write_text(json.dumps(release()))
|
||||
image = 'gcr.forust.xyz/forust/session-keeper'
|
||||
line = f'image: {image}@sha256:{"e" * 64}\n'
|
||||
result = io.StringIO()
|
||||
with patch.dict(os.environ, {'RELEASE_FILE': str(path), 'DEPLOY_SHA': 'a' * 40}):
|
||||
release_module.render(io.StringIO(line), result)
|
||||
self.assertEqual(result.getvalue(), line)
|
||||
with self.assertRaises(ValueError):
|
||||
release_module.render(io.StringIO(f'image: {image}:prod\n'), io.StringIO())
|
||||
|
||||
def test_unknown_image_cannot_emit_partial_manifest(self):
|
||||
with tempfile.TemporaryDirectory() as scratch:
|
||||
path = Path(scratch) / 'release.json'
|
||||
@@ -107,10 +94,13 @@ class ArtifactTests(unittest.TestCase):
|
||||
patch.object(release_module, 'command', side_effect=fake_command),
|
||||
patch.object(subprocess, 'run', return_value=subprocess.CompletedProcess([], 0)),
|
||||
):
|
||||
release_module.build(root / 'release.json')
|
||||
plan = root / 'plan.json'
|
||||
release_module.prepare_images(plan)
|
||||
for name in release_module.IMAGES:
|
||||
release_module.build(root / f'{name}.json', name, plan)
|
||||
self.assertEqual(built, ['errorpages/Dockerfile'])
|
||||
self.assertTrue(all(not directory.exists() for directory in auth_directories))
|
||||
self.assertEqual(json.loads((root / 'release.json').read_text())['sha'], 'e' * 40)
|
||||
self.assertEqual(json.loads((root / 'error-pages.json').read_text())['sha'], 'e' * 40)
|
||||
|
||||
|
||||
class DurableRunTests(unittest.TestCase):
|
||||
@@ -218,7 +208,7 @@ class FailureSummaryTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as scratch:
|
||||
summary = Path(scratch) / 'summary.md'
|
||||
|
||||
def failed_build(_output, report):
|
||||
def failed_build(_output, report, _name, _plan):
|
||||
report.update(phase='Build or reuse xdfnx-homepage', current='xdfnx-homepage', built=['error-pages'])
|
||||
report['images']['gcr.forust.xyz/forust/error-pages'] = 'sha256:' + 'b' * 64
|
||||
raise RuntimeError('private value must not appear in the summary')
|
||||
@@ -228,7 +218,7 @@ class FailureSummaryTests(unittest.TestCase):
|
||||
patch.object(release_module, 'build_images', side_effect=failed_build),
|
||||
self.assertRaises(RuntimeError),
|
||||
):
|
||||
release_module.build(Path(scratch) / 'release.json')
|
||||
release_module.build(Path(scratch) / 'release.json', 'xdfnx-homepage', Path('plan.json'))
|
||||
content = summary.read_text()
|
||||
self.assertIn('**failure**', content)
|
||||
self.assertIn('error-pages', content)
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Matrix release contracts and failure gates without a registry."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, call, patch
|
||||
|
||||
from test_cicd import release, release_module
|
||||
|
||||
|
||||
def plan_data(changed):
|
||||
targets = []
|
||||
for name, (context, dockerfile) in release_module.IMAGES.items():
|
||||
targets.append(
|
||||
{
|
||||
'name': name,
|
||||
'image': f'gcr.forust.xyz/forust/{name}',
|
||||
'context': context,
|
||||
'dockerfile': dockerfile,
|
||||
'inputs': ('d' if name in changed else 'c') * 64,
|
||||
'reuse_digest': None if name in changed else 'sha256:' + 'b' * 64,
|
||||
}
|
||||
)
|
||||
return {'sha': 'a' * 40, 'targets': targets}
|
||||
|
||||
|
||||
class MatrixTests(unittest.TestCase):
|
||||
def test_no_change_one_image_all_images_and_missing_baseline(self):
|
||||
for changed in (set(), {'error-pages'}, set(release_module.IMAGES)):
|
||||
with self.subTest(changed=changed), tempfile.TemporaryDirectory() as scratch:
|
||||
api = Mock()
|
||||
api.successful_runs.return_value = iter([{'id': 1}])
|
||||
api.release.return_value = release()
|
||||
expected = plan_data(changed)
|
||||
fingerprints = {t['dockerfile']: t['inputs'] for t in expected['targets']}
|
||||
output = Path(scratch) / 'plan.json'
|
||||
with (
|
||||
patch.dict(os.environ, {'GITHUB_SHA': 'a' * 40, 'GITHUB_RUN_ID': '2'}),
|
||||
patch.object(release_module, 'command', return_value='a' * 40),
|
||||
patch.object(release_module, 'Gitea', return_value=api),
|
||||
patch.object(
|
||||
release_module, 'fingerprint', side_effect=lambda _c, f, mapping=fingerprints: mapping[f]
|
||||
),
|
||||
):
|
||||
release_module.prepare_images(output)
|
||||
self.assertEqual(json.loads(output.read_text()), expected)
|
||||
api.successful_runs.return_value = iter([])
|
||||
with (
|
||||
tempfile.TemporaryDirectory() as scratch,
|
||||
patch.dict(os.environ, {'GITHUB_SHA': 'a' * 40}),
|
||||
patch.object(release_module, 'command', return_value='a' * 40),
|
||||
patch.object(release_module, 'Gitea', return_value=api),
|
||||
patch.object(release_module, 'fingerprint', return_value='c' * 64),
|
||||
):
|
||||
output = Path(scratch) / 'plan.json'
|
||||
release_module.prepare_images(output)
|
||||
self.assertTrue(all(t['reuse_digest'] is None for t in json.loads(output.read_text())['targets']))
|
||||
|
||||
def test_incomplete_or_wrong_sha_fragments_cannot_publish_tags(self):
|
||||
for wrong_sha in (False, True):
|
||||
with self.subTest(wrong_sha=wrong_sha), tempfile.TemporaryDirectory() as scratch:
|
||||
root = Path(scratch)
|
||||
plan = root / 'plan.json'
|
||||
plan.write_text(json.dumps(plan_data(set())))
|
||||
for name in release_module.IMAGES:
|
||||
if name == 'xdfnx-homepage' and not wrong_sha:
|
||||
continue
|
||||
folder = root / f'image-{name}'
|
||||
folder.mkdir()
|
||||
image = f'gcr.forust.xyz/forust/{name}'
|
||||
folder.joinpath('image.json').write_text(
|
||||
json.dumps(
|
||||
{
|
||||
'version': 1,
|
||||
'sha': ('e' if wrong_sha else 'a') * 40,
|
||||
'images': {image: 'sha256:' + 'b' * 64},
|
||||
'inputs': {image: 'c' * 64},
|
||||
}
|
||||
)
|
||||
)
|
||||
with (
|
||||
patch.object(release_module, 'checked_plan', return_value=plan_data(set())),
|
||||
patch.object(release_module.subprocess, 'run') as execute,
|
||||
self.assertRaises((ValueError, FileNotFoundError)),
|
||||
):
|
||||
release_module.finalize_images(root / 'release.json', root, plan)
|
||||
execute.assert_not_called()
|
||||
self.assertFalse((root / 'release.json').exists())
|
||||
|
||||
def test_manifest_only_release_pins_each_successful_digest(self):
|
||||
with tempfile.TemporaryDirectory() as scratch:
|
||||
root = Path(scratch)
|
||||
data = plan_data(set())
|
||||
for target in data['targets']:
|
||||
folder = root / f'image-{target["name"]}'
|
||||
folder.mkdir()
|
||||
image = target['image']
|
||||
folder.joinpath('image.json').write_text(
|
||||
json.dumps(
|
||||
{
|
||||
'version': 1,
|
||||
'sha': data['sha'],
|
||||
'images': {image: target['reuse_digest']},
|
||||
'inputs': {image: target['inputs']},
|
||||
}
|
||||
)
|
||||
)
|
||||
with (
|
||||
patch.object(release_module, 'checked_plan', return_value=data),
|
||||
patch.dict(os.environ, {'REGISTRY_USERNAME': 'test', 'REGISTRY_PASSWORD': 'placeholder'}),
|
||||
patch.object(release_module.subprocess, 'run'),
|
||||
patch.object(release_module, 'command') as execute,
|
||||
):
|
||||
release_module.finalize_images(root / 'release.json', root, root / 'plan.json')
|
||||
self.assertEqual(
|
||||
[entry.args for entry in execute.call_args_list],
|
||||
[
|
||||
call(
|
||||
'docker',
|
||||
'buildx',
|
||||
'imagetools',
|
||||
'create',
|
||||
'--prefer-index=false',
|
||||
'--tag',
|
||||
f'{t["image"]}:sha-{data["sha"]}',
|
||||
f'{t["image"]}@{t["reuse_digest"]}',
|
||||
).args
|
||||
for t in data['targets']
|
||||
],
|
||||
)
|
||||
self.assertEqual(json.loads((root / 'release.json').read_text()), release())
|
||||
|
||||
def test_checkout_mismatch_cannot_build(self):
|
||||
with tempfile.TemporaryDirectory() as scratch:
|
||||
plan = Path(scratch) / 'plan.json'
|
||||
plan.write_text(json.dumps(plan_data(set())))
|
||||
with (
|
||||
patch.dict(os.environ, {'GITHUB_SHA': 'e' * 40}),
|
||||
patch.object(release_module, 'command', return_value='a' * 40),
|
||||
self.assertRaisesRegex(ValueError, 'source commit'),
|
||||
):
|
||||
release_module.checked_plan(plan)
|
||||
Reference in new issue
Block a user