certlord 1.0.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. certlord-1.0.0rc1/.github/scripts/check-release.py +32 -0
  2. certlord-1.0.0rc1/.github/scripts/check-test-collection.py +138 -0
  3. certlord-1.0.0rc1/.github/scripts/install-lifecycle-tools.sh +24 -0
  4. certlord-1.0.0rc1/.github/tests/test_collection_guard.py +68 -0
  5. certlord-1.0.0rc1/.github/workflows/debian.yml +158 -0
  6. certlord-1.0.0rc1/.github/workflows/lifecycle.yml +39 -0
  7. certlord-1.0.0rc1/.github/workflows/pypi.yml +98 -0
  8. certlord-1.0.0rc1/.github/workflows/tests.yml +56 -0
  9. certlord-1.0.0rc1/AGENTS.md +31 -0
  10. certlord-1.0.0rc1/CHANGELOG +9 -0
  11. certlord-1.0.0rc1/CONTRIBUTING.md +32 -0
  12. certlord-1.0.0rc1/LICENSE +674 -0
  13. certlord-1.0.0rc1/MANIFEST.in +22 -0
  14. certlord-1.0.0rc1/MIGRATION.md +201 -0
  15. certlord-1.0.0rc1/PKG-INFO +132 -0
  16. certlord-1.0.0rc1/README.md +85 -0
  17. certlord-1.0.0rc1/RELEASE +1 -0
  18. certlord-1.0.0rc1/VERSION +1 -0
  19. certlord-1.0.0rc1/assets/brand/README.md +37 -0
  20. certlord-1.0.0rc1/assets/brand/favicon/android-chrome-192x192.png +0 -0
  21. certlord-1.0.0rc1/assets/brand/favicon/android-chrome-512x512.png +0 -0
  22. certlord-1.0.0rc1/assets/brand/favicon/apple-touch-icon.png +0 -0
  23. certlord-1.0.0rc1/assets/brand/favicon/favicon-16x16.png +0 -0
  24. certlord-1.0.0rc1/assets/brand/favicon/favicon-32x32.png +0 -0
  25. certlord-1.0.0rc1/assets/brand/favicon/favicon-48x48.png +0 -0
  26. certlord-1.0.0rc1/assets/brand/favicon/favicon.ico +0 -0
  27. certlord-1.0.0rc1/assets/brand/favicon/favicon.svg +9 -0
  28. certlord-1.0.0rc1/assets/brand/favicon/site.webmanifest +19 -0
  29. certlord-1.0.0rc1/assets/brand/png/icon-1024.png +0 -0
  30. certlord-1.0.0rc1/assets/brand/png/icon-128.png +0 -0
  31. certlord-1.0.0rc1/assets/brand/png/icon-256.png +0 -0
  32. certlord-1.0.0rc1/assets/brand/png/icon-512.png +0 -0
  33. certlord-1.0.0rc1/assets/brand/png/icon-dark-512.png +0 -0
  34. certlord-1.0.0rc1/assets/brand/png/icon-mono-black-512.png +0 -0
  35. certlord-1.0.0rc1/assets/brand/png/icon-mono-white-512.png +0 -0
  36. certlord-1.0.0rc1/assets/brand/png/logo-horizontal-1200.png +0 -0
  37. certlord-1.0.0rc1/assets/brand/png/logo-horizontal-dark-1200.png +0 -0
  38. certlord-1.0.0rc1/assets/brand/png/logo-horizontal-mono-black-1200.png +0 -0
  39. certlord-1.0.0rc1/assets/brand/png/logo-horizontal-mono-white-1200.png +0 -0
  40. certlord-1.0.0rc1/assets/brand/png/logo-stacked-1200.png +0 -0
  41. certlord-1.0.0rc1/assets/brand/png/logo-stacked-dark-1200.png +0 -0
  42. certlord-1.0.0rc1/assets/brand/png/logo-stacked-mono-black-1200.png +0 -0
  43. certlord-1.0.0rc1/assets/brand/png/logo-stacked-mono-white-1200.png +0 -0
  44. certlord-1.0.0rc1/assets/brand/png/wordmark-1200.png +0 -0
  45. certlord-1.0.0rc1/assets/brand/png/wordmark-dark-1200.png +0 -0
  46. certlord-1.0.0rc1/assets/brand/png/wordmark-mono-black-1200.png +0 -0
  47. certlord-1.0.0rc1/assets/brand/png/wordmark-mono-white-1200.png +0 -0
  48. certlord-1.0.0rc1/assets/brand/svg/icon-dark.svg +9 -0
  49. certlord-1.0.0rc1/assets/brand/svg/icon-mono-black.svg +9 -0
  50. certlord-1.0.0rc1/assets/brand/svg/icon-mono-white.svg +9 -0
  51. certlord-1.0.0rc1/assets/brand/svg/icon.svg +9 -0
  52. certlord-1.0.0rc1/assets/brand/svg/logo-horizontal-dark.svg +17 -0
  53. certlord-1.0.0rc1/assets/brand/svg/logo-horizontal-mono-black.svg +17 -0
  54. certlord-1.0.0rc1/assets/brand/svg/logo-horizontal-mono-white.svg +17 -0
  55. certlord-1.0.0rc1/assets/brand/svg/logo-horizontal.svg +17 -0
  56. certlord-1.0.0rc1/assets/brand/svg/logo-stacked-dark.svg +17 -0
  57. certlord-1.0.0rc1/assets/brand/svg/logo-stacked-mono-black.svg +17 -0
  58. certlord-1.0.0rc1/assets/brand/svg/logo-stacked-mono-white.svg +17 -0
  59. certlord-1.0.0rc1/assets/brand/svg/logo-stacked.svg +17 -0
  60. certlord-1.0.0rc1/assets/brand/svg/wordmark-dark.svg +9 -0
  61. certlord-1.0.0rc1/assets/brand/svg/wordmark-mono-black.svg +9 -0
  62. certlord-1.0.0rc1/assets/brand/svg/wordmark-mono-white.svg +9 -0
  63. certlord-1.0.0rc1/assets/brand/svg/wordmark.svg +9 -0
  64. certlord-1.0.0rc1/bin/certlord +163 -0
  65. certlord-1.0.0rc1/certlord/__init__.py +4 -0
  66. certlord-1.0.0rc1/certlord/adapters/__init__.py +1 -0
  67. certlord-1.0.0rc1/certlord/adapters/api_auth.py +21 -0
  68. certlord-1.0.0rc1/certlord/adapters/auton_receipt.py +71 -0
  69. certlord-1.0.0rc1/certlord/adapters/commands.py +122 -0
  70. certlord-1.0.0rc1/certlord/adapters/cycle_progress.py +77 -0
  71. certlord-1.0.0rc1/certlord/adapters/deployment_lease.py +51 -0
  72. certlord-1.0.0rc1/certlord/adapters/deployment_queue.py +40 -0
  73. certlord-1.0.0rc1/certlord/adapters/dns_policy.py +120 -0
  74. certlord-1.0.0rc1/certlord/adapters/issuance_command.py +38 -0
  75. certlord-1.0.0rc1/certlord/adapters/operations.py +69 -0
  76. certlord-1.0.0rc1/certlord/adapters/pending.py +99 -0
  77. certlord-1.0.0rc1/certlord/adapters/processes.py +75 -0
  78. certlord-1.0.0rc1/certlord/adapters/redis_client.py +38 -0
  79. certlord-1.0.0rc1/certlord/adapters/tls_probe.py +42 -0
  80. certlord-1.0.0rc1/certlord/adapters/tls_verification.py +91 -0
  81. certlord-1.0.0rc1/certlord/adapters/vault_auth.py +43 -0
  82. certlord-1.0.0rc1/certlord/adapters/vault_store.py +123 -0
  83. certlord-1.0.0rc1/certlord/classes/__init__.py +28 -0
  84. certlord-1.0.0rc1/certlord/classes/certbot_handler.py +166 -0
  85. certlord-1.0.0rc1/certlord/classes/config.py +73 -0
  86. certlord-1.0.0rc1/certlord/classes/deployer.py +179 -0
  87. certlord-1.0.0rc1/certlord/classes/exceptions.py +19 -0
  88. certlord-1.0.0rc1/certlord/classes/ssl_cert_auto_object.py +79 -0
  89. certlord-1.0.0rc1/certlord/classes/ssl_checker.py +374 -0
  90. certlord-1.0.0rc1/certlord/classes/vault_crawler.py +192 -0
  91. certlord-1.0.0rc1/certlord/client/__init__.py +1 -0
  92. certlord-1.0.0rc1/certlord/client/api.py +71 -0
  93. certlord-1.0.0rc1/certlord/client/cli.py +100 -0
  94. certlord-1.0.0rc1/certlord/client/tui.py +53 -0
  95. certlord-1.0.0rc1/certlord/composition.py +83 -0
  96. certlord-1.0.0rc1/certlord/modules/__init__.py +28 -0
  97. certlord-1.0.0rc1/certlord/modules/letsencrypt.py +157 -0
  98. certlord-1.0.0rc1/certlord/modules/route_auth.py +24 -0
  99. certlord-1.0.0rc1/certlord/modules/ssl_certs.py +214 -0
  100. certlord-1.0.0rc1/certlord/ports/__init__.py +1 -0
  101. certlord-1.0.0rc1/certlord/ports/certificates.py +51 -0
  102. certlord-1.0.0rc1/certlord/services/__init__.py +1 -0
  103. certlord-1.0.0rc1/certlord/services/api_access.py +18 -0
  104. certlord-1.0.0rc1/certlord/services/certificate_ids.py +40 -0
  105. certlord-1.0.0rc1/certlord/services/certificates.py +367 -0
  106. certlord-1.0.0rc1/certlord/services/cycle_progress.py +69 -0
  107. certlord-1.0.0rc1/certlord/services/identity_definition.py +32 -0
  108. certlord-1.0.0rc1/certlord/services/import_material.py +89 -0
  109. certlord-1.0.0rc1/certlord/services/issuance.py +7 -0
  110. certlord-1.0.0rc1/certlord/services/monitoring.py +75 -0
  111. certlord-1.0.0rc1/certlord/services/observations.py +141 -0
  112. certlord-1.0.0rc1/certlord/services/operation_tracking.py +86 -0
  113. certlord-1.0.0rc1/certlord/services/runtime.py +260 -0
  114. certlord-1.0.0rc1/certlord/services/verification.py +25 -0
  115. certlord-1.0.0rc1/certlord.egg-info/PKG-INFO +132 -0
  116. certlord-1.0.0rc1/certlord.egg-info/SOURCES.txt +202 -0
  117. certlord-1.0.0rc1/certlord.egg-info/dependency_links.txt +1 -0
  118. certlord-1.0.0rc1/certlord.egg-info/requires.txt +14 -0
  119. certlord-1.0.0rc1/certlord.egg-info/top_level.txt +1 -0
  120. certlord-1.0.0rc1/constraints-minimum.txt +15 -0
  121. certlord-1.0.0rc1/debian/changelog +9 -0
  122. certlord-1.0.0rc1/debian/compat +1 -0
  123. certlord-1.0.0rc1/debian/control +15 -0
  124. certlord-1.0.0rc1/debian/copyright +28 -0
  125. certlord-1.0.0rc1/debian/default +3 -0
  126. certlord-1.0.0rc1/debian/dirs +2 -0
  127. certlord-1.0.0rc1/debian/docs +1 -0
  128. certlord-1.0.0rc1/debian/init +65 -0
  129. certlord-1.0.0rc1/debian/install +3 -0
  130. certlord-1.0.0rc1/debian/links +1 -0
  131. certlord-1.0.0rc1/debian/logrotate +10 -0
  132. certlord-1.0.0rc1/debian/postinst +21 -0
  133. certlord-1.0.0rc1/debian/requirements.txt +4 -0
  134. certlord-1.0.0rc1/debian/rules +25 -0
  135. certlord-1.0.0rc1/debian/service +19 -0
  136. certlord-1.0.0rc1/docs/acme-connector.md +117 -0
  137. certlord-1.0.0rc1/docs/architecture.md +421 -0
  138. certlord-1.0.0rc1/docs/certificate-identity.md +102 -0
  139. certlord-1.0.0rc1/docs/certificate-import.md +139 -0
  140. certlord-1.0.0rc1/docs/components.md +57 -0
  141. certlord-1.0.0rc1/docs/debian-installation.md +140 -0
  142. certlord-1.0.0rc1/docs/operation-correlation.md +97 -0
  143. certlord-1.0.0rc1/docs/operations.md +226 -0
  144. certlord-1.0.0rc1/docs/release-notes.md +39 -0
  145. certlord-1.0.0rc1/docs/supervision.md +92 -0
  146. certlord-1.0.0rc1/docs/testing.md +50 -0
  147. certlord-1.0.0rc1/docs/tls-verification.md +90 -0
  148. certlord-1.0.0rc1/docs/vault-approle.md +63 -0
  149. certlord-1.0.0rc1/etc/certlord/certbot-httpreq.yml.example +21 -0
  150. certlord-1.0.0rc1/etc/certlord/certlord.yml +180 -0
  151. certlord-1.0.0rc1/etc/certlord/credentials.yml.example +15 -0
  152. certlord-1.0.0rc1/etc/certlord/vault-policy.hcl.example +19 -0
  153. certlord-1.0.0rc1/etc/sudoers.d/90-certlord +2 -0
  154. certlord-1.0.0rc1/pyproject.toml +3 -0
  155. certlord-1.0.0rc1/requirements.txt +14 -0
  156. certlord-1.0.0rc1/scripts/build_brand_assets.py +36 -0
  157. certlord-1.0.0rc1/setup.cfg +7 -0
  158. certlord-1.0.0rc1/setup.py +45 -0
  159. certlord-1.0.0rc1/setup.yml +24 -0
  160. certlord-1.0.0rc1/setup.yml.j2 +24 -0
  161. certlord-1.0.0rc1/tests/contracts/test_components.py +174 -0
  162. certlord-1.0.0rc1/tests/import_fixture.py +28 -0
  163. certlord-1.0.0rc1/tests/integration/auton-routes.yml +43 -0
  164. certlord-1.0.0rc1/tests/integration/lifecycle.py +2128 -0
  165. certlord-1.0.0rc1/tests/integration/network_faults.py +163 -0
  166. certlord-1.0.0rc1/tests/integration/systemd.Dockerfile +17 -0
  167. certlord-1.0.0rc1/tests/integration/systemd_acceptance.py +171 -0
  168. certlord-1.0.0rc1/tests/integration/systemd_process.py +68 -0
  169. certlord-1.0.0rc1/tests/integration/vault-fixture.service +5 -0
  170. certlord-1.0.0rc1/tests/test_authenticated_routes.py +36 -0
  171. certlord-1.0.0rc1/tests/test_auton_receipt.py +118 -0
  172. certlord-1.0.0rc1/tests/test_backend_timeouts.py +160 -0
  173. certlord-1.0.0rc1/tests/test_boundaries.py +113 -0
  174. certlord-1.0.0rc1/tests/test_certificate_ids.py +96 -0
  175. certlord-1.0.0rc1/tests/test_certificate_import.py +137 -0
  176. certlord-1.0.0rc1/tests/test_certificate_replacement.py +117 -0
  177. certlord-1.0.0rc1/tests/test_certificates.py +93 -0
  178. certlord-1.0.0rc1/tests/test_challenge_expiry.py +41 -0
  179. certlord-1.0.0rc1/tests/test_cli.py +90 -0
  180. certlord-1.0.0rc1/tests/test_commands.py +41 -0
  181. certlord-1.0.0rc1/tests/test_crawler_retry.py +60 -0
  182. certlord-1.0.0rc1/tests/test_cycle_progress.py +128 -0
  183. certlord-1.0.0rc1/tests/test_daemon_permissions.py +48 -0
  184. certlord-1.0.0rc1/tests/test_deployer.py +86 -0
  185. certlord-1.0.0rc1/tests/test_deployment_coordination.py +93 -0
  186. certlord-1.0.0rc1/tests/test_deployment_queue.py +40 -0
  187. certlord-1.0.0rc1/tests/test_http_errors.py +83 -0
  188. certlord-1.0.0rc1/tests/test_identity_fingerprint.py +133 -0
  189. certlord-1.0.0rc1/tests/test_issuance.py +81 -0
  190. certlord-1.0.0rc1/tests/test_monitoring_deletion.py +44 -0
  191. certlord-1.0.0rc1/tests/test_monitoring_startup.py +61 -0
  192. certlord-1.0.0rc1/tests/test_monitoring_transport.py +76 -0
  193. certlord-1.0.0rc1/tests/test_observations.py +147 -0
  194. certlord-1.0.0rc1/tests/test_operation_tracking.py +132 -0
  195. certlord-1.0.0rc1/tests/test_operations.py +147 -0
  196. certlord-1.0.0rc1/tests/test_processes.py +93 -0
  197. certlord-1.0.0rc1/tests/test_removal_coordination.py +88 -0
  198. certlord-1.0.0rc1/tests/test_reverse_index.py +26 -0
  199. certlord-1.0.0rc1/tests/test_save_coordination.py +72 -0
  200. certlord-1.0.0rc1/tests/test_tls_probe_transport.py +81 -0
  201. certlord-1.0.0rc1/tests/test_tls_verification.py +129 -0
  202. certlord-1.0.0rc1/tests/test_vault_removal.py +52 -0
  203. certlord-1.0.0rc1/tests/test_worker_log_safety.py +79 -0
@@ -0,0 +1,32 @@
1
+ """Reject mismatched versions, tags or private documents before publication."""
2
+ import ast
3
+ import os
4
+ from pathlib import Path, PurePosixPath
5
+ import tarfile
6
+ import zipfile
7
+ import yaml
8
+
9
+ root = Path(__file__).resolve().parents[2]
10
+ version = (root / 'VERSION').read_text().strip()
11
+ config = yaml.safe_load((root / 'setup.yml').read_text())
12
+ assert config['version'] == config['release'] == version
13
+ assert (root / 'RELEASE').read_text().strip() == version
14
+ module = ast.parse((root / 'bin/certlord').read_text())
15
+ cli_version = next(ast.literal_eval(n.value) for n in module.body
16
+ if isinstance(n, ast.Assign) and any(isinstance(t, ast.Name) and t.id == '__version__' for t in n.targets))
17
+ assert cli_version == version
18
+ ref = os.environ.get('GITHUB_REF', '')
19
+ if ref.startswith('refs/tags/'):
20
+ assert ref == 'refs/tags/v' + version, (ref, version)
21
+ files = sorted((root / 'dist').iterdir())
22
+ assert len(files) == 2 and sum(p.suffix == '.whl' for p in files) == 1
23
+ assert sum(p.name.endswith('.tar.gz') for p in files) == 1
24
+ for path in files:
25
+ assert version in path.name, path.name
26
+ names = zipfile.ZipFile(path).namelist() if path.suffix == '.whl' else tarfile.open(path).getnames()
27
+ for name in names:
28
+ parts = PurePosixPath(name).parts
29
+ assert not name.startswith('/') and '..' not in parts, name
30
+ assert not any(part in {'.git', 'ROADMAP.md', 'SANITIZED-MANIFEST.json', 'pr-history.json', 'evidence', 'framework_candidates'} for part in parts), name
31
+ assert not any(part.startswith(('review-20', 'release-readiness-', 'http-boundary-review-', 'operation-audit-review-')) for part in parts), name
32
+ print('Release metadata, tag and distribution inventory verified:', version)
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ """Compare test declarations with actual collection (Python 3.8+ CI helper).
3
+
4
+ Run before the normal test command, with the same interpreter and environment.
5
+ Pass every separately discovered directory together; skipped tests still count
6
+ as collected. This does not execute tests or replace the normal test command.
7
+ """
8
+ import argparse
9
+ import ast
10
+ import inspect
11
+ from pathlib import Path
12
+ import sys
13
+ import unittest
14
+
15
+
16
+ TEST_PATTERN = 'test*.py'
17
+ FUNCTION_NODES = (ast.FunctionDef, ast.AsyncFunctionDef)
18
+
19
+
20
+ def declarations(directories):
21
+ expected = set()
22
+ files = {path.resolve() for directory in directories
23
+ for path in directory.rglob(TEST_PATTERN)}
24
+ for path in sorted(files):
25
+ tree = ast.parse(path.read_bytes(), filename=str(path))
26
+
27
+ def visit(nodes, prefix=''):
28
+ for node in nodes:
29
+ if isinstance(node, FUNCTION_NODES):
30
+ if node.name.startswith('test'):
31
+ key = (path, prefix + node.name)
32
+ if key in expected:
33
+ raise ValueError('Duplicate test declaration: %s:%s' % key)
34
+ expected.add(key)
35
+ # Helpers defined inside a test are not separate tests.
36
+ elif isinstance(node, ast.ClassDef):
37
+ visit(node.body, prefix + node.name + '.')
38
+ else:
39
+ # Include declarations behind module/class-level conditions.
40
+ visit(list(ast.iter_child_nodes(node)), prefix)
41
+
42
+ visit(tree.body)
43
+ return expected
44
+
45
+
46
+ def identity(obj):
47
+ obj = inspect.unwrap(obj)
48
+ source = inspect.getsourcefile(obj)
49
+ return (Path(source).resolve(), obj.__qualname__) if source else None
50
+
51
+
52
+ def unittest_collection(directories):
53
+ collected = set()
54
+ count = 0
55
+
56
+ def cases(suite):
57
+ for item in suite:
58
+ if isinstance(item, unittest.TestSuite):
59
+ yield from cases(item)
60
+ else:
61
+ yield item
62
+
63
+ for directory in directories:
64
+ loader = unittest.TestLoader()
65
+ suite = loader.discover(str(directory), pattern=TEST_PATTERN)
66
+ if loader.errors:
67
+ raise ValueError('\n'.join(loader.errors))
68
+ items = list(cases(suite))
69
+ if not items:
70
+ raise ValueError('Empty test suite: %s' % directory)
71
+ count += len(items)
72
+ for case in items:
73
+ method = getattr(case, case._testMethodName)
74
+ collected.add(identity(method))
75
+ return collected, count
76
+
77
+
78
+ def pytest_collection(directories):
79
+ import pytest
80
+
81
+ class Collector:
82
+ def __init__(self):
83
+ self.collected = set()
84
+ self.counts = {directory: 0 for directory in directories}
85
+ self.count = 0
86
+
87
+ def pytest_collection_finish(self, session):
88
+ self.count = len(session.items)
89
+ for item in session.items:
90
+ self.collected.add(identity(item.obj))
91
+ for directory in directories:
92
+ if directory in Path(str(item.path)).resolve().parents:
93
+ self.counts[directory] += 1
94
+
95
+ collector = Collector()
96
+ status = pytest.main(['--collect-only', '-q'] +
97
+ [str(directory) for directory in directories],
98
+ plugins=[collector])
99
+ if status != 0:
100
+ raise ValueError('pytest collection failed with exit code %s' % status)
101
+ for directory, count in collector.counts.items():
102
+ if not count:
103
+ raise ValueError('Empty test suite: %s' % directory)
104
+ return collector.collected, collector.count
105
+
106
+
107
+ def main():
108
+ parser = argparse.ArgumentParser(description=__doc__)
109
+ parser.add_argument('--runner', choices=('unittest', 'pytest'), required=True)
110
+ parser.add_argument('directories', type=Path, nargs='+')
111
+ args = parser.parse_args()
112
+ # Match python -m unittest/pytest: use the working directory, never the
113
+ # script repository. Installed-package jobs run from outside the checkout.
114
+ sys.path.insert(0, str(Path.cwd()))
115
+ directories = [directory.resolve() for directory in args.directories]
116
+ try:
117
+ for directory in directories:
118
+ if not directory.is_dir():
119
+ raise ValueError('Missing test directory: %s' % directory)
120
+ expected = declarations(directories)
121
+ collect = pytest_collection if args.runner == 'pytest' else unittest_collection
122
+ collected, count = collect(directories)
123
+ missing = expected - collected
124
+ if missing:
125
+ raise ValueError('Tests not collected by %s:\n%s' % (
126
+ args.runner, '\n'.join('%s:%s' % key for key in sorted(missing))))
127
+ if not expected:
128
+ raise ValueError('No test declarations found')
129
+ print('Collection verified: %d declarations, %d cases (%s)' %
130
+ (len(expected), count, args.runner))
131
+ except (ImportError, OSError, SyntaxError, ValueError) as exc:
132
+ print(str(exc), file=sys.stderr)
133
+ return 1
134
+ return 0
135
+
136
+
137
+ if __name__ == '__main__':
138
+ sys.exit(main())
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bash
2
+ # Disposable Linux amd64 integration tools; no installation into the host PATH.
3
+ set -euo pipefail
4
+ fixture_dir="${1:?Usage: install-lifecycle-tools.sh DIRECTORY}"
5
+ mkdir -p "$fixture_dir"
6
+ cd "$fixture_dir"
7
+ for binary in pebble pebble-challtestsrv; do
8
+ curl --fail --silent --show-error --location --retry 2 --max-time 120 \
9
+ "https://github.com/letsencrypt/pebble/releases/download/v2.10.1/${binary}-linux-amd64.tar.gz" \
10
+ --output "$binary.tar.gz"
11
+ done
12
+ curl --fail --silent --show-error --location --retry 2 --max-time 120 \
13
+ https://releases.hashicorp.com/vault/1.20.4/vault_1.20.4_linux_amd64.zip --output vault.zip
14
+ sha256sum --check <<'SUMS'
15
+ 4f2fcb5bca8c85c9cf73ad140fccfc0d2be40bd81ab99879c79b7b8a0b4f70ed pebble.tar.gz
16
+ e93a5aa25ecdf3af2f9fbb2de32b0173e64a2eae81002a4ccfe35fa6f4f60b92 pebble-challtestsrv.tar.gz
17
+ fc5fb5d01d192f1216b139fb5c6af17e3af742aaeffc289fd861920ec55f2c9c vault.zip
18
+ SUMS
19
+ for binary in pebble pebble-challtestsrv; do
20
+ tar --no-same-owner -xzf "$binary.tar.gz"
21
+ cp "$binary-linux-amd64/linux/amd64/$binary" "$binary"
22
+ chmod +x "$binary"
23
+ done
24
+ unzip -q vault.zip vault
@@ -0,0 +1,68 @@
1
+ """Regression tests for false-green collection failures."""
2
+ import importlib.util
3
+ from pathlib import Path
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ import unittest
8
+
9
+
10
+ GUARD = Path(__file__).resolve().parents[1] / 'scripts' / 'check-test-collection.py'
11
+ CASE = 'import unittest\nclass Example(unittest.TestCase):\n def test_ok(self): pass\n'
12
+
13
+
14
+ class CollectionGuardTests(unittest.TestCase):
15
+ def check(self, files, directories=('tests',), runner='unittest', succeeds=False):
16
+ with tempfile.TemporaryDirectory() as temporary:
17
+ root = Path(temporary)
18
+ for directory in directories:
19
+ (root / directory).mkdir(parents=True, exist_ok=True)
20
+ for name, content in files.items():
21
+ path = root / name
22
+ path.parent.mkdir(parents=True, exist_ok=True)
23
+ path.write_text(content)
24
+ result = subprocess.run(
25
+ [sys.executable, str(GUARD), '--runner', runner] + list(directories),
26
+ cwd=str(root), stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
27
+ universal_newlines=True, timeout=30)
28
+ self.assertEqual(result.returncode == 0, succeeds, result.stdout)
29
+ return result.stdout
30
+
31
+ def test_valid_and_skipped_cases_are_collected(self):
32
+ self.check({'tests/test_valid.py': CASE +
33
+ ' @unittest.skip("requires integration service")\n'
34
+ ' def test_skipped(self): pass\n'}, succeeds=True)
35
+
36
+ def test_unittest_rejects_ignored_function_next_to_valid_case(self):
37
+ output = self.check({'tests/test_mixed.py': CASE + '\ndef test_lost(): pass\n'})
38
+ self.assertIn('test_lost', output)
39
+
40
+ def test_empty_suite_fails(self):
41
+ self.assertIn('Empty test suite', self.check({}))
42
+
43
+ def test_nested_directory_requires_separate_discovery(self):
44
+ files = {'tests/test_outer.py': CASE, 'tests/contracts/test_inner.py': CASE}
45
+ self.assertIn('not collected', self.check(files))
46
+ self.check(files, directories=('tests', 'tests/contracts'), succeeds=True)
47
+
48
+ def test_import_error_fails(self):
49
+ self.assertIn('ImportError', self.check({
50
+ 'tests/test_broken.py': 'raise ImportError("broken dependency")\n'}))
51
+
52
+ def test_duplicate_definition_fails(self):
53
+ self.assertIn('Duplicate test declaration', self.check({
54
+ 'tests/test_duplicate.py': CASE + ' def test_ok(self): pass\n'}))
55
+
56
+ def test_plain_class_is_not_a_unittest_case(self):
57
+ self.assertIn('TestLost.test_lost', self.check({
58
+ 'tests/test_plain.py': CASE + '\nclass TestLost:\n def test_lost(self): pass\n'}))
59
+
60
+ @unittest.skipUnless(importlib.util.find_spec('pytest'), 'pytest runner only')
61
+ def test_pytest_parameterization_and_deselection(self):
62
+ files = {'tests/test_parameters.py':
63
+ 'import pytest\n@pytest.mark.parametrize("value", [1, 2])\n'
64
+ 'def test_value(value): assert value > 0\n'}
65
+ self.check(files, runner='pytest', succeeds=True)
66
+ files['tests/conftest.py'] = (
67
+ 'def pytest_collection_modifyitems(items):\n items.clear()\n')
68
+ self.check(files, runner='pytest')
@@ -0,0 +1,158 @@
1
+ name: Debian packaging
2
+ on:
3
+ workflow_call:
4
+ pull_request:
5
+ workflow_dispatch:
6
+ permissions:
7
+ contents: read
8
+ jobs:
9
+ debian12:
10
+ runs-on: ubuntu-latest
11
+ timeout-minutes: 20
12
+ container: debian:12
13
+ steps:
14
+ - name: Install build tools
15
+ run: |
16
+ apt-get update -qq
17
+ apt-get install -y --no-install-recommends ca-certificates git build-essential debhelper dh-virtualenv python3 python3-venv python3-dev python3-setuptools python3-yaml libcurl4-openssl-dev libssl-dev
18
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19
+ - name: Build Debian package with isolated dependencies
20
+ run: |
21
+ dpkg-buildpackage -us -uc -b
22
+ mkdir -p deb-artifacts
23
+ cp ../certlord_*.deb deb-artifacts/
24
+ - name: Install without network access to package indexes
25
+ run: |
26
+ export PIP_NO_INDEX=1
27
+ export http_proxy=http://127.0.0.1:9
28
+ export https_proxy=http://127.0.0.1:9
29
+ dpkg -i ../certlord_*.deb
30
+ /opt/venvs/certlord/bin/python -X faulthandler -c 'import sys; print(sys.version, flush=True); import pycurl; print("pycurl imported", flush=True); import cryptography; print("cryptography imported", flush=True)'
31
+ PYTHONFAULTHANDLER=1 /usr/bin/certlord --help
32
+ /opt/venvs/certlord/bin/python -m pip check
33
+ /usr/bin/python3 -I -c 'import importlib.util; assert importlib.util.find_spec("certlord") is None'
34
+ test ! -e /etc/certlord/certlord.yml
35
+ test ! -e /etc/systemd/system/multi-user.target.wants/certlord.service
36
+ test ! -e /etc/sudoers.d/90-certlord
37
+ test "$(stat -c %U /opt/venvs/certlord)" = root
38
+ test "$(stat -c %U /var/lib/certlord)" = certlord
39
+ - name: Installed regression suites outside source checkout
40
+ run: |
41
+ suite_dir=$(mktemp -d)
42
+ cp -R tests etc "$suite_dir/"
43
+ cp .github/scripts/check-test-collection.py "$suite_dir/"
44
+ cd "$suite_dir"
45
+ /opt/venvs/certlord/bin/python check-test-collection.py --runner unittest tests tests/contracts
46
+ /opt/venvs/certlord/bin/python -m unittest discover -s tests -q
47
+ /opt/venvs/certlord/bin/python -m unittest discover -s tests/contracts -q
48
+ - name: Reconfigure and remove without deleting local state
49
+ run: |
50
+ touch /var/lib/certlord/retained-fixture
51
+ PIP_NO_INDEX=1 dpkg -i ../certlord_*.deb
52
+ dpkg --remove certlord
53
+ test -f /var/lib/certlord/retained-fixture
54
+ test ! -e /usr/bin/certlord
55
+ - uses: actions/upload-artifact@v4
56
+ with:
57
+ name: certlord-debian12-amd64
58
+ path: deb-artifacts/*.deb
59
+ if-no-files-found: error
60
+ retention-days: 7
61
+ clean-runtime:
62
+ needs: debian12
63
+ runs-on: ubuntu-latest
64
+ timeout-minutes: 10
65
+ container: debian:12
66
+ steps:
67
+ - uses: actions/download-artifact@v4
68
+ with:
69
+ name: certlord-debian12-amd64
70
+ path: /tmp/certlord-package
71
+ - name: Install on clean runtime without build dependencies
72
+ run: |
73
+ apt-get update -qq
74
+ apt-get install -y --no-install-recommends /tmp/certlord-package/certlord_*.deb
75
+ cd /tmp
76
+ PYTHONFAULTHANDLER=1 /usr/bin/certlord --help
77
+ /opt/venvs/certlord/bin/certbot --version
78
+ /opt/venvs/certlord/bin/auton --help
79
+ /opt/venvs/certlord/bin/python -m pip check
80
+ /usr/bin/python3 -I -c 'import importlib.util; assert importlib.util.find_spec("certlord") is None'
81
+ test ! -e /etc/certlord/certlord.yml
82
+ test ! -e /etc/sudoers.d/90-certlord
83
+ test ! -e /etc/systemd/system/multi-user.target.wants/certlord.service
84
+ systemd-runtime:
85
+ needs: debian12
86
+ runs-on: ubuntu-latest
87
+ timeout-minutes: 12
88
+ steps:
89
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
90
+ - uses: actions/download-artifact@v4
91
+ with:
92
+ name: certlord-debian12-amd64
93
+ path: tests/integration/package
94
+ - name: Fetch checksum-verified disposable Vault
95
+ run: |
96
+ curl --fail --silent --show-error --location --retry 2 --max-time 120 https://releases.hashicorp.com/vault/1.20.4/vault_1.20.4_linux_amd64.zip --output "$RUNNER_TEMP/vault.zip"
97
+ echo "fc5fb5d01d192f1216b139fb5c6af17e3af742aaeffc289fd861920ec55f2c9c $RUNNER_TEMP/vault.zip" | sha256sum --check
98
+ unzip -q "$RUNNER_TEMP/vault.zip" vault -d tests/integration
99
+ - name: Build disposable systemd acceptance image
100
+ run: docker build --target service -f tests/integration/systemd.Dockerfile -t certlord-systemd-fixture tests/integration
101
+ - name: Boot systemd and verify installed service
102
+ run: |
103
+ mkdir -p "$RUNNER_TEMP/systemd-evidence"
104
+ docker run -d --name certlord-systemd --privileged --cgroupns=private --tmpfs /run --tmpfs /run/lock --mount "type=bind,src=$RUNNER_TEMP/systemd-evidence,dst=/evidence" certlord-systemd-fixture
105
+ timeout 45 bash -c 'until docker exec certlord-systemd systemctl list-units --no-pager >/dev/null 2>&1; do sleep 1; done'
106
+ docker exec certlord-systemd /opt/venvs/certlord/bin/python /fixture/systemd_acceptance.py
107
+ - name: Retain sanitized systemd evidence
108
+ if: always()
109
+ uses: actions/upload-artifact@v4
110
+ with:
111
+ name: certlord-systemd-${{ github.run_attempt }}
112
+ path: ${{ runner.temp }}/systemd-evidence/systemd.json
113
+ if-no-files-found: warn
114
+ retention-days: 7
115
+ - name: Remove disposable acceptance container
116
+ if: always()
117
+ run: |
118
+ if docker container inspect certlord-systemd >/dev/null 2>&1; then
119
+ docker rm -f certlord-systemd
120
+ fi
121
+ systemd-lifecycle:
122
+ needs: debian12
123
+ runs-on: ubuntu-latest
124
+ timeout-minutes: 15
125
+ steps:
126
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
127
+ - uses: actions/download-artifact@v4
128
+ with:
129
+ name: certlord-debian12-amd64
130
+ path: tests/integration/package
131
+ - name: Prepare checksum-verified disposable tools and examples
132
+ run: |
133
+ bash .github/scripts/install-lifecycle-tools.sh "$GITHUB_WORKSPACE/tests/integration/tools"
134
+ cp tests/integration/tools/vault tests/integration/vault
135
+ mkdir tests/integration/source
136
+ cp -R etc tests/integration/source/
137
+ - name: Build installed-service lifecycle image
138
+ run: docker build --target lifecycle -f tests/integration/systemd.Dockerfile -t certlord-systemd-lifecycle tests/integration
139
+ - name: Interrupt actual issuance and deployment through systemctl
140
+ run: |
141
+ mkdir -p "$RUNNER_TEMP/systemd-lifecycle-evidence"
142
+ docker run -d --name certlord-systemd-lifecycle --privileged --cgroupns=private --tmpfs /run --tmpfs /run/lock --mount "type=bind,src=$RUNNER_TEMP/systemd-lifecycle-evidence,dst=/evidence" certlord-systemd-lifecycle
143
+ timeout 45 bash -c 'until docker exec certlord-systemd-lifecycle systemctl list-units --no-pager >/dev/null 2>&1; do sleep 1; done'
144
+ docker exec -e CERTLORD_DISPOSABLE_SYSTEMD=1 -e PATH=/opt/venvs/certlord/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin certlord-systemd-lifecycle /opt/venvs/certlord/bin/python /fixture/lifecycle.py run --systemd-scheduler --tools /fixture/tools --source /fixture/source --report /evidence/report.json
145
+ - name: Retain sanitized systemd lifecycle evidence
146
+ if: always()
147
+ uses: actions/upload-artifact@v4
148
+ with:
149
+ name: certlord-systemd-lifecycle-${{ github.run_attempt }}
150
+ path: ${{ runner.temp }}/systemd-lifecycle-evidence/report.json
151
+ if-no-files-found: warn
152
+ retention-days: 7
153
+ - name: Remove disposable lifecycle container
154
+ if: always()
155
+ run: |
156
+ if docker container inspect certlord-systemd-lifecycle >/dev/null 2>&1; then
157
+ docker rm -f certlord-systemd-lifecycle
158
+ fi
@@ -0,0 +1,39 @@
1
+ name: lifecycle acceptance
2
+ on:
3
+ workflow_call:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+ workflow_dispatch:
8
+ permissions:
9
+ contents: read
10
+ jobs:
11
+ lifecycle:
12
+ runs-on: ubuntu-latest
13
+ timeout-minutes: 15
14
+ steps:
15
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
16
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
17
+ with:
18
+ python-version: '3.12'
19
+ - name: Install runtime and pinned external clients
20
+ run: |
21
+ sudo apt-get update -qq
22
+ sudo apt-get install -y redis-server unzip
23
+ python -m pip install . 'certbot-httpreq==0.0.24' 'acme-http-connector==0.2.1' 'certbot==5.8.0' 'auton==1.2.1' 'autond==1.2.1'
24
+ - name: Install checksum-verified disposable CA and Vault
25
+ run: bash .github/scripts/install-lifecycle-tools.sh "$RUNNER_TEMP/certlord-tools"
26
+ - name: Installed lifecycle, actual served TLS and failure recovery
27
+ working-directory: ${{ runner.temp }}
28
+ run: >-
29
+ python "$GITHUB_WORKSPACE/tests/integration/lifecycle.py" run
30
+ --tools "$RUNNER_TEMP/certlord-tools" --source "$GITHUB_WORKSPACE"
31
+ --report "$RUNNER_TEMP/certlord-evidence/report.json"
32
+ - name: Retain only sanitized test evidence
33
+ if: always()
34
+ uses: actions/upload-artifact@v4
35
+ with:
36
+ name: certlord-lifecycle-${{ github.run_attempt }}
37
+ path: ${{ runner.temp }}/certlord-evidence/report.json
38
+ if-no-files-found: warn
39
+ retention-days: 7
@@ -0,0 +1,98 @@
1
+ name: Release candidate and PyPI publication
2
+ on:
3
+ workflow_dispatch:
4
+ push:
5
+ tags: ['v*']
6
+ pull_request:
7
+ permissions:
8
+ contents: read
9
+ concurrency:
10
+ group: publish-${{ github.ref }}
11
+ cancel-in-progress: false
12
+ jobs:
13
+ tests:
14
+ uses: ./.github/workflows/tests.yml
15
+ lifecycle:
16
+ uses: ./.github/workflows/lifecycle.yml
17
+ debian:
18
+ uses: ./.github/workflows/debian.yml
19
+ build:
20
+ needs: [tests, lifecycle, debian]
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 15
23
+ steps:
24
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
25
+ with:
26
+ persist-credentials: false
27
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
28
+ with:
29
+ python-version: '3.12'
30
+ - run: python -m pip install build twine PyYAML
31
+ - run: python -m build
32
+ - run: python -m twine check --strict dist/*
33
+ - run: python .github/scripts/check-release.py
34
+ - uses: actions/upload-artifact@v4
35
+ with:
36
+ name: release-distributions
37
+ path: dist/
38
+ if-no-files-found: error
39
+ tag:
40
+ needs: build
41
+ if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') && github.repository == 'decryptus/certlord' && !github.event.repository.private
42
+ runs-on: ubuntu-latest
43
+ permissions:
44
+ contents: write
45
+ outputs:
46
+ version: ${{ steps.tag.outputs.version }}
47
+ steps:
48
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
49
+ with:
50
+ persist-credentials: false
51
+ - id: tag
52
+ env:
53
+ GH_TOKEN: ${{ github.token }}
54
+ run: |
55
+ set -eu
56
+ version=$(cat VERSION)
57
+ echo "version=$version" >> "$GITHUB_OUTPUT"
58
+ tag="v$version"
59
+ if existing=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.sha'); then
60
+ test "$existing" = "$GITHUB_SHA"
61
+ else
62
+ gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/$tag" -f sha="$GITHUB_SHA"
63
+ fi
64
+ publish:
65
+ needs: [build, tag]
66
+ if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') && github.repository == 'decryptus/certlord' && !github.event.repository.private
67
+ runs-on: ubuntu-latest
68
+ environment: pypi
69
+ permissions:
70
+ id-token: write
71
+ steps:
72
+ - uses: actions/download-artifact@v4
73
+ with:
74
+ name: release-distributions
75
+ path: dist/
76
+ - uses: pypa/gh-action-pypi-publish@release/v1
77
+ github-release:
78
+ needs: [publish, tag]
79
+ runs-on: ubuntu-latest
80
+ permissions:
81
+ contents: write
82
+ steps:
83
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
84
+ with:
85
+ persist-credentials: false
86
+ - uses: actions/download-artifact@v4
87
+ with:
88
+ name: release-distributions
89
+ path: dist/
90
+ - name: Publish matching GitHub release
91
+ env:
92
+ GH_TOKEN: ${{ github.token }}
93
+ RELEASE_TAG: v${{ needs.tag.outputs.version }}
94
+ run: |
95
+ set -eu
96
+ prerelease_flag=''
97
+ case "$RELEASE_TAG" in *rc*|*a[0-9]*|*b[0-9]*) prerelease_flag='--prerelease';; esac
98
+ gh release create "$RELEASE_TAG" dist/* --verify-tag --title "CertLord $RELEASE_TAG" --notes-file docs/release-notes.md $prerelease_flag
@@ -0,0 +1,56 @@
1
+ name: Tests
2
+ on:
3
+ workflow_call:
4
+ push:
5
+ pull_request:
6
+ permissions:
7
+ contents: read
8
+ jobs:
9
+ tests:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python: ['3.11', '3.12']
14
+ dependencies: ['minimum', 'latest']
15
+ steps:
16
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
17
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
18
+ with:
19
+ python-version: ${{ matrix.python }}
20
+ - name: Install dependencies
21
+ run: |
22
+ if [ "${{ matrix.dependencies }}" = "minimum" ]; then
23
+ python -m pip install -r requirements.txt -c constraints-minimum.txt build
24
+ else
25
+ python -m pip install -r requirements.txt build
26
+ fi
27
+ python -m pip check
28
+ - name: Verify collection and its failure guards
29
+ run: |
30
+ python .github/scripts/check-test-collection.py --runner unittest tests tests/contracts
31
+ python .github/scripts/check-test-collection.py --runner unittest .github/tests
32
+ python -m unittest discover -s .github/tests -v
33
+ - name: Unit tests
34
+ run: python -m unittest discover -s tests -v
35
+ - name: Component contracts
36
+ run: python -m unittest discover -s tests/contracts -v
37
+ - name: Build source archive and wheel
38
+ run: python -m build
39
+ - name: Install built wheel
40
+ run: |
41
+ python -m pip install --force-reinstall --no-deps dist/*.whl
42
+ python -m pip check
43
+ - name: Installed CLI smoke test
44
+ working-directory: /tmp
45
+ run: certlord --help
46
+
47
+
48
+ - name: Installed-package regression suites outside checkout
49
+ working-directory: /tmp
50
+ run: |
51
+ suite_dir=$(mktemp -d)
52
+ cp -R "$GITHUB_WORKSPACE/tests" "$GITHUB_WORKSPACE/etc" "$suite_dir/"
53
+ cd "$suite_dir"
54
+ python "$GITHUB_WORKSPACE/.github/scripts/check-test-collection.py" --runner unittest tests tests/contracts
55
+ python -m unittest discover -s tests -v
56
+ python -m unittest discover -s tests/contracts -v
@@ -0,0 +1,31 @@
1
+ # Engineering requirements
2
+
3
+ These are maintainer requirements.
4
+
5
+ ## Test discovery and execution
6
+
7
+ - Verify the actual runner and every discovery root before adding or changing
8
+ tests. A green command does not prove that all test declarations were loaded.
9
+ Do not put standalone pytest functions into a suite run only by unittest.
10
+ - Run `.github/scripts/check-test-collection.py` with Python, the same
11
+ interpreter/environment as the tests, and the declared runner. Pass separately
12
+ discovered directories together when they are nested. The guard compares
13
+ declarations in `test*.py` files with actual collection; it does not run tests.
14
+ - Reject empty suites, import/collection errors, duplicate definitions and
15
+ declarations omitted by the runner. Keep test helpers out of the `test*`
16
+ namespace. Explain intentional skips and separate integration prerequisites;
17
+ never hide failures with `continue-on-error` or `|| true`.
18
+ - Run the normal test command after the guard. Report collected/executed/skipped
19
+ counts and investigate unexpected changes; collection alone is not a passing
20
+ test run. Parameterization may produce several cases per declaration.
21
+ - Check test paths, naming patterns, selection filters and CI commands together.
22
+ A new test directory or non-Python test harness needs an explicit CI entry;
23
+ this guard only covers the directories and Python naming pattern passed to it.
24
+ - Verify installed-package tests outside the source checkout where applicable.
25
+ Architecture scans must resolve the package under test and reject empty scans.
26
+ - Preserve supported interpreter matrices. Changing runners requires an explicit
27
+ decision and collection parity; this Python 3.8+ CI helper does not replace
28
+ legacy-interpreter execution or integration/system acceptance tests.
29
+
30
+ Current project runner: `unittest` for `tests` and `tests/contracts`. CI helper tests use
31
+ `unittest` in `.github/tests`.
@@ -0,0 +1,9 @@
1
+ certlord (1.0.0~rc1) UNRELEASED; urgency=medium
2
+
3
+ * Prepare the first public release candidate, Python version 1.0.0rc1.
4
+ * ACME issuance/renewal and external PEM import/versioned replacement.
5
+ * Vault KV v2 storage, authenticated HTTP API, CLI and explicit TUI.
6
+ * Auton deployment, optional remote receipts and served TLS verification.
7
+ * Operational observations, bounded shutdown and recovery checks.
8
+
9
+ -- Adrien Delle Cave <pypi@doowan.net> Sat, 03 Oct 2026 00:45:00 +0000