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.
- certlord-1.0.0rc1/.github/scripts/check-release.py +32 -0
- certlord-1.0.0rc1/.github/scripts/check-test-collection.py +138 -0
- certlord-1.0.0rc1/.github/scripts/install-lifecycle-tools.sh +24 -0
- certlord-1.0.0rc1/.github/tests/test_collection_guard.py +68 -0
- certlord-1.0.0rc1/.github/workflows/debian.yml +158 -0
- certlord-1.0.0rc1/.github/workflows/lifecycle.yml +39 -0
- certlord-1.0.0rc1/.github/workflows/pypi.yml +98 -0
- certlord-1.0.0rc1/.github/workflows/tests.yml +56 -0
- certlord-1.0.0rc1/AGENTS.md +31 -0
- certlord-1.0.0rc1/CHANGELOG +9 -0
- certlord-1.0.0rc1/CONTRIBUTING.md +32 -0
- certlord-1.0.0rc1/LICENSE +674 -0
- certlord-1.0.0rc1/MANIFEST.in +22 -0
- certlord-1.0.0rc1/MIGRATION.md +201 -0
- certlord-1.0.0rc1/PKG-INFO +132 -0
- certlord-1.0.0rc1/README.md +85 -0
- certlord-1.0.0rc1/RELEASE +1 -0
- certlord-1.0.0rc1/VERSION +1 -0
- certlord-1.0.0rc1/assets/brand/README.md +37 -0
- certlord-1.0.0rc1/assets/brand/favicon/android-chrome-192x192.png +0 -0
- certlord-1.0.0rc1/assets/brand/favicon/android-chrome-512x512.png +0 -0
- certlord-1.0.0rc1/assets/brand/favicon/apple-touch-icon.png +0 -0
- certlord-1.0.0rc1/assets/brand/favicon/favicon-16x16.png +0 -0
- certlord-1.0.0rc1/assets/brand/favicon/favicon-32x32.png +0 -0
- certlord-1.0.0rc1/assets/brand/favicon/favicon-48x48.png +0 -0
- certlord-1.0.0rc1/assets/brand/favicon/favicon.ico +0 -0
- certlord-1.0.0rc1/assets/brand/favicon/favicon.svg +9 -0
- certlord-1.0.0rc1/assets/brand/favicon/site.webmanifest +19 -0
- certlord-1.0.0rc1/assets/brand/png/icon-1024.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/icon-128.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/icon-256.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/icon-512.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/icon-dark-512.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/icon-mono-black-512.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/icon-mono-white-512.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/logo-horizontal-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/logo-horizontal-dark-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/logo-horizontal-mono-black-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/logo-horizontal-mono-white-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/logo-stacked-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/logo-stacked-dark-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/logo-stacked-mono-black-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/logo-stacked-mono-white-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/wordmark-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/wordmark-dark-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/wordmark-mono-black-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/png/wordmark-mono-white-1200.png +0 -0
- certlord-1.0.0rc1/assets/brand/svg/icon-dark.svg +9 -0
- certlord-1.0.0rc1/assets/brand/svg/icon-mono-black.svg +9 -0
- certlord-1.0.0rc1/assets/brand/svg/icon-mono-white.svg +9 -0
- certlord-1.0.0rc1/assets/brand/svg/icon.svg +9 -0
- certlord-1.0.0rc1/assets/brand/svg/logo-horizontal-dark.svg +17 -0
- certlord-1.0.0rc1/assets/brand/svg/logo-horizontal-mono-black.svg +17 -0
- certlord-1.0.0rc1/assets/brand/svg/logo-horizontal-mono-white.svg +17 -0
- certlord-1.0.0rc1/assets/brand/svg/logo-horizontal.svg +17 -0
- certlord-1.0.0rc1/assets/brand/svg/logo-stacked-dark.svg +17 -0
- certlord-1.0.0rc1/assets/brand/svg/logo-stacked-mono-black.svg +17 -0
- certlord-1.0.0rc1/assets/brand/svg/logo-stacked-mono-white.svg +17 -0
- certlord-1.0.0rc1/assets/brand/svg/logo-stacked.svg +17 -0
- certlord-1.0.0rc1/assets/brand/svg/wordmark-dark.svg +9 -0
- certlord-1.0.0rc1/assets/brand/svg/wordmark-mono-black.svg +9 -0
- certlord-1.0.0rc1/assets/brand/svg/wordmark-mono-white.svg +9 -0
- certlord-1.0.0rc1/assets/brand/svg/wordmark.svg +9 -0
- certlord-1.0.0rc1/bin/certlord +163 -0
- certlord-1.0.0rc1/certlord/__init__.py +4 -0
- certlord-1.0.0rc1/certlord/adapters/__init__.py +1 -0
- certlord-1.0.0rc1/certlord/adapters/api_auth.py +21 -0
- certlord-1.0.0rc1/certlord/adapters/auton_receipt.py +71 -0
- certlord-1.0.0rc1/certlord/adapters/commands.py +122 -0
- certlord-1.0.0rc1/certlord/adapters/cycle_progress.py +77 -0
- certlord-1.0.0rc1/certlord/adapters/deployment_lease.py +51 -0
- certlord-1.0.0rc1/certlord/adapters/deployment_queue.py +40 -0
- certlord-1.0.0rc1/certlord/adapters/dns_policy.py +120 -0
- certlord-1.0.0rc1/certlord/adapters/issuance_command.py +38 -0
- certlord-1.0.0rc1/certlord/adapters/operations.py +69 -0
- certlord-1.0.0rc1/certlord/adapters/pending.py +99 -0
- certlord-1.0.0rc1/certlord/adapters/processes.py +75 -0
- certlord-1.0.0rc1/certlord/adapters/redis_client.py +38 -0
- certlord-1.0.0rc1/certlord/adapters/tls_probe.py +42 -0
- certlord-1.0.0rc1/certlord/adapters/tls_verification.py +91 -0
- certlord-1.0.0rc1/certlord/adapters/vault_auth.py +43 -0
- certlord-1.0.0rc1/certlord/adapters/vault_store.py +123 -0
- certlord-1.0.0rc1/certlord/classes/__init__.py +28 -0
- certlord-1.0.0rc1/certlord/classes/certbot_handler.py +166 -0
- certlord-1.0.0rc1/certlord/classes/config.py +73 -0
- certlord-1.0.0rc1/certlord/classes/deployer.py +179 -0
- certlord-1.0.0rc1/certlord/classes/exceptions.py +19 -0
- certlord-1.0.0rc1/certlord/classes/ssl_cert_auto_object.py +79 -0
- certlord-1.0.0rc1/certlord/classes/ssl_checker.py +374 -0
- certlord-1.0.0rc1/certlord/classes/vault_crawler.py +192 -0
- certlord-1.0.0rc1/certlord/client/__init__.py +1 -0
- certlord-1.0.0rc1/certlord/client/api.py +71 -0
- certlord-1.0.0rc1/certlord/client/cli.py +100 -0
- certlord-1.0.0rc1/certlord/client/tui.py +53 -0
- certlord-1.0.0rc1/certlord/composition.py +83 -0
- certlord-1.0.0rc1/certlord/modules/__init__.py +28 -0
- certlord-1.0.0rc1/certlord/modules/letsencrypt.py +157 -0
- certlord-1.0.0rc1/certlord/modules/route_auth.py +24 -0
- certlord-1.0.0rc1/certlord/modules/ssl_certs.py +214 -0
- certlord-1.0.0rc1/certlord/ports/__init__.py +1 -0
- certlord-1.0.0rc1/certlord/ports/certificates.py +51 -0
- certlord-1.0.0rc1/certlord/services/__init__.py +1 -0
- certlord-1.0.0rc1/certlord/services/api_access.py +18 -0
- certlord-1.0.0rc1/certlord/services/certificate_ids.py +40 -0
- certlord-1.0.0rc1/certlord/services/certificates.py +367 -0
- certlord-1.0.0rc1/certlord/services/cycle_progress.py +69 -0
- certlord-1.0.0rc1/certlord/services/identity_definition.py +32 -0
- certlord-1.0.0rc1/certlord/services/import_material.py +89 -0
- certlord-1.0.0rc1/certlord/services/issuance.py +7 -0
- certlord-1.0.0rc1/certlord/services/monitoring.py +75 -0
- certlord-1.0.0rc1/certlord/services/observations.py +141 -0
- certlord-1.0.0rc1/certlord/services/operation_tracking.py +86 -0
- certlord-1.0.0rc1/certlord/services/runtime.py +260 -0
- certlord-1.0.0rc1/certlord/services/verification.py +25 -0
- certlord-1.0.0rc1/certlord.egg-info/PKG-INFO +132 -0
- certlord-1.0.0rc1/certlord.egg-info/SOURCES.txt +202 -0
- certlord-1.0.0rc1/certlord.egg-info/dependency_links.txt +1 -0
- certlord-1.0.0rc1/certlord.egg-info/requires.txt +14 -0
- certlord-1.0.0rc1/certlord.egg-info/top_level.txt +1 -0
- certlord-1.0.0rc1/constraints-minimum.txt +15 -0
- certlord-1.0.0rc1/debian/changelog +9 -0
- certlord-1.0.0rc1/debian/compat +1 -0
- certlord-1.0.0rc1/debian/control +15 -0
- certlord-1.0.0rc1/debian/copyright +28 -0
- certlord-1.0.0rc1/debian/default +3 -0
- certlord-1.0.0rc1/debian/dirs +2 -0
- certlord-1.0.0rc1/debian/docs +1 -0
- certlord-1.0.0rc1/debian/init +65 -0
- certlord-1.0.0rc1/debian/install +3 -0
- certlord-1.0.0rc1/debian/links +1 -0
- certlord-1.0.0rc1/debian/logrotate +10 -0
- certlord-1.0.0rc1/debian/postinst +21 -0
- certlord-1.0.0rc1/debian/requirements.txt +4 -0
- certlord-1.0.0rc1/debian/rules +25 -0
- certlord-1.0.0rc1/debian/service +19 -0
- certlord-1.0.0rc1/docs/acme-connector.md +117 -0
- certlord-1.0.0rc1/docs/architecture.md +421 -0
- certlord-1.0.0rc1/docs/certificate-identity.md +102 -0
- certlord-1.0.0rc1/docs/certificate-import.md +139 -0
- certlord-1.0.0rc1/docs/components.md +57 -0
- certlord-1.0.0rc1/docs/debian-installation.md +140 -0
- certlord-1.0.0rc1/docs/operation-correlation.md +97 -0
- certlord-1.0.0rc1/docs/operations.md +226 -0
- certlord-1.0.0rc1/docs/release-notes.md +39 -0
- certlord-1.0.0rc1/docs/supervision.md +92 -0
- certlord-1.0.0rc1/docs/testing.md +50 -0
- certlord-1.0.0rc1/docs/tls-verification.md +90 -0
- certlord-1.0.0rc1/docs/vault-approle.md +63 -0
- certlord-1.0.0rc1/etc/certlord/certbot-httpreq.yml.example +21 -0
- certlord-1.0.0rc1/etc/certlord/certlord.yml +180 -0
- certlord-1.0.0rc1/etc/certlord/credentials.yml.example +15 -0
- certlord-1.0.0rc1/etc/certlord/vault-policy.hcl.example +19 -0
- certlord-1.0.0rc1/etc/sudoers.d/90-certlord +2 -0
- certlord-1.0.0rc1/pyproject.toml +3 -0
- certlord-1.0.0rc1/requirements.txt +14 -0
- certlord-1.0.0rc1/scripts/build_brand_assets.py +36 -0
- certlord-1.0.0rc1/setup.cfg +7 -0
- certlord-1.0.0rc1/setup.py +45 -0
- certlord-1.0.0rc1/setup.yml +24 -0
- certlord-1.0.0rc1/setup.yml.j2 +24 -0
- certlord-1.0.0rc1/tests/contracts/test_components.py +174 -0
- certlord-1.0.0rc1/tests/import_fixture.py +28 -0
- certlord-1.0.0rc1/tests/integration/auton-routes.yml +43 -0
- certlord-1.0.0rc1/tests/integration/lifecycle.py +2128 -0
- certlord-1.0.0rc1/tests/integration/network_faults.py +163 -0
- certlord-1.0.0rc1/tests/integration/systemd.Dockerfile +17 -0
- certlord-1.0.0rc1/tests/integration/systemd_acceptance.py +171 -0
- certlord-1.0.0rc1/tests/integration/systemd_process.py +68 -0
- certlord-1.0.0rc1/tests/integration/vault-fixture.service +5 -0
- certlord-1.0.0rc1/tests/test_authenticated_routes.py +36 -0
- certlord-1.0.0rc1/tests/test_auton_receipt.py +118 -0
- certlord-1.0.0rc1/tests/test_backend_timeouts.py +160 -0
- certlord-1.0.0rc1/tests/test_boundaries.py +113 -0
- certlord-1.0.0rc1/tests/test_certificate_ids.py +96 -0
- certlord-1.0.0rc1/tests/test_certificate_import.py +137 -0
- certlord-1.0.0rc1/tests/test_certificate_replacement.py +117 -0
- certlord-1.0.0rc1/tests/test_certificates.py +93 -0
- certlord-1.0.0rc1/tests/test_challenge_expiry.py +41 -0
- certlord-1.0.0rc1/tests/test_cli.py +90 -0
- certlord-1.0.0rc1/tests/test_commands.py +41 -0
- certlord-1.0.0rc1/tests/test_crawler_retry.py +60 -0
- certlord-1.0.0rc1/tests/test_cycle_progress.py +128 -0
- certlord-1.0.0rc1/tests/test_daemon_permissions.py +48 -0
- certlord-1.0.0rc1/tests/test_deployer.py +86 -0
- certlord-1.0.0rc1/tests/test_deployment_coordination.py +93 -0
- certlord-1.0.0rc1/tests/test_deployment_queue.py +40 -0
- certlord-1.0.0rc1/tests/test_http_errors.py +83 -0
- certlord-1.0.0rc1/tests/test_identity_fingerprint.py +133 -0
- certlord-1.0.0rc1/tests/test_issuance.py +81 -0
- certlord-1.0.0rc1/tests/test_monitoring_deletion.py +44 -0
- certlord-1.0.0rc1/tests/test_monitoring_startup.py +61 -0
- certlord-1.0.0rc1/tests/test_monitoring_transport.py +76 -0
- certlord-1.0.0rc1/tests/test_observations.py +147 -0
- certlord-1.0.0rc1/tests/test_operation_tracking.py +132 -0
- certlord-1.0.0rc1/tests/test_operations.py +147 -0
- certlord-1.0.0rc1/tests/test_processes.py +93 -0
- certlord-1.0.0rc1/tests/test_removal_coordination.py +88 -0
- certlord-1.0.0rc1/tests/test_reverse_index.py +26 -0
- certlord-1.0.0rc1/tests/test_save_coordination.py +72 -0
- certlord-1.0.0rc1/tests/test_tls_probe_transport.py +81 -0
- certlord-1.0.0rc1/tests/test_tls_verification.py +129 -0
- certlord-1.0.0rc1/tests/test_vault_removal.py +52 -0
- 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
|