conclear 1.0.0__py3-none-any.whl
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.
- conclear/__init__.py +5 -0
- conclear/__main__.py +6 -0
- conclear/_embedded_identity.py +3 -0
- conclear/adapters/__init__.py +1 -0
- conclear/adapters/base.py +107 -0
- conclear/adapters/buildah.py +128 -0
- conclear/adapters/ci.py +198 -0
- conclear/adapters/cosign.py +475 -0
- conclear/adapters/git.py +127 -0
- conclear/adapters/hadolint.py +84 -0
- conclear/adapters/podman.py +661 -0
- conclear/adapters/quay.py +492 -0
- conclear/adapters/registry_backends.py +100 -0
- conclear/adapters/skopeo.py +193 -0
- conclear/adapters/trivy.py +402 -0
- conclear/archive.py +340 -0
- conclear/archive_source.py +113 -0
- conclear/artifacts.py +710 -0
- conclear/attestations.py +111 -0
- conclear/build_identity.py +18 -0
- conclear/catalog.py +415 -0
- conclear/checks.py +687 -0
- conclear/cli.py +229 -0
- conclear/commands/__init__.py +1 -0
- conclear/commands/adopt.py +64 -0
- conclear/commands/archive.py +143 -0
- conclear/commands/common.py +209 -0
- conclear/commands/configuration.py +39 -0
- conclear/commands/local.py +540 -0
- conclear/commands/maintenance.py +836 -0
- conclear/commands/remote.py +462 -0
- conclear/commands/transport.py +88 -0
- conclear/commands/version.py +32 -0
- conclear/compatibility_inventory.py +260 -0
- conclear/config.py +1611 -0
- conclear/config_decisions.py +53 -0
- conclear/conformance.py +37 -0
- conclear/containerfile.py +325 -0
- conclear/context.py +381 -0
- conclear/data/__init__.py +1 -0
- conclear/data/checks.json +76 -0
- conclear/data/guide-options.json +133 -0
- conclear/data/guide-requirements.json +3045 -0
- conclear/data/implementation.json +408 -0
- conclear/data/requirement-coverage.json +244 -0
- conclear/database.py +129 -0
- conclear/dependencies.py +267 -0
- conclear/emulation.py +138 -0
- conclear/errors.py +119 -0
- conclear/fileio.py +119 -0
- conclear/freshness.py +146 -0
- conclear/guide_options.py +200 -0
- conclear/guide_requirements.py +567 -0
- conclear/hook_scratch.py +132 -0
- conclear/hooks.py +139 -0
- conclear/identity.py +77 -0
- conclear/implementation.py +367 -0
- conclear/jsonutil.py +158 -0
- conclear/layout_assembly.py +269 -0
- conclear/oci.py +480 -0
- conclear/parsing.py +97 -0
- conclear/path_safety.py +135 -0
- conclear/pin_application.py +393 -0
- conclear/pin_occurrences.py +292 -0
- conclear/pin_updates.py +572 -0
- conclear/pins.py +327 -0
- conclear/presentation.py +101 -0
- conclear/process.py +532 -0
- conclear/provenance.py +129 -0
- conclear/records.py +223 -0
- conclear/registry_control.py +101 -0
- conclear/registry_policy.py +165 -0
- conclear/release_check.py +737 -0
- conclear/release_profile.py +362 -0
- conclear/rescan_history.py +337 -0
- conclear/runtime.py +204 -0
- conclear/runtime_directory.py +197 -0
- conclear/scan_identity.py +86 -0
- conclear/scan_policy.py +508 -0
- conclear/schema.py +125 -0
- conclear/schemas/__init__.py +1 -0
- conclear/schemas/archive.schema.json +40 -0
- conclear/schemas/config.schema.json +398 -0
- conclear/schemas/profile.schema.json +128 -0
- conclear/schemas/proposal.schema.json +113 -0
- conclear/schemas/provenance.schema.json +99 -0
- conclear/schemas/record.schema.json +869 -0
- conclear/schemas/result.schema.json +2482 -0
- conclear/schemas/triage.schema.json +43 -0
- conclear/secrets.py +127 -0
- conclear/services/__init__.py +1 -0
- conclear/services/adoption.py +126 -0
- conclear/services/adoption_draft.py +443 -0
- conclear/services/adoption_observation.py +459 -0
- conclear/services/archive_rescan.py +130 -0
- conclear/services/archives.py +519 -0
- conclear/services/assembly.py +788 -0
- conclear/services/attestation.py +567 -0
- conclear/services/attestation_reads.py +41 -0
- conclear/services/checking.py +89 -0
- conclear/services/ci_context.py +135 -0
- conclear/services/cleanup.py +462 -0
- conclear/services/configuration_view.py +309 -0
- conclear/services/doctor.py +159 -0
- conclear/services/local_phases.py +105 -0
- conclear/services/preflight.py +178 -0
- conclear/services/privilege_tests.py +392 -0
- conclear/services/promotion.py +335 -0
- conclear/services/publication.py +428 -0
- conclear/services/qualification.py +724 -0
- conclear/services/qualification_inputs.py +128 -0
- conclear/services/registry_diagnostics.py +210 -0
- conclear/services/release.py +672 -0
- conclear/services/rescan.py +675 -0
- conclear/services/rescan_evidence.py +145 -0
- conclear/services/run_context.py +340 -0
- conclear/services/runtime_controls.py +133 -0
- conclear/services/runtime_lifecycle.py +804 -0
- conclear/services/runtime_tests.py +795 -0
- conclear/services/verification.py +363 -0
- conclear/setid_inventory.py +303 -0
- conclear/source_integrity.py +161 -0
- conclear/spdx.py +174 -0
- conclear/test_inputs.py +309 -0
- conclear/test_output_archive.py +92 -0
- conclear/tool_matrix.py +93 -0
- conclear/tools.py +324 -0
- conclear/transport.py +981 -0
- conclear/triage.py +110 -0
- conclear/values.py +264 -0
- conclear/version_sources.py +148 -0
- conclear/workspace.py +628 -0
- conclear-1.0.0.dist-info/METADATA +735 -0
- conclear-1.0.0.dist-info/RECORD +137 -0
- conclear-1.0.0.dist-info/WHEEL +4 -0
- conclear-1.0.0.dist-info/entry_points.txt +3 -0
- conclear-1.0.0.dist-info/licenses/LICENSES/GPL-3.0-or-later.txt +232 -0
conclear/__init__.py
ADDED
conclear/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Typed adapters for release tools and external services."""
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Shared command construction for external tool adapters."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from conclear.errors import InvalidInvocationError, OperationalError
|
|
8
|
+
from conclear.process import (
|
|
9
|
+
CommandRequest,
|
|
10
|
+
OperationKind,
|
|
11
|
+
ProcessResult,
|
|
12
|
+
)
|
|
13
|
+
from conclear.tools import ResolvedTool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def prepare_new_layout_path(layout_path: Path) -> None:
|
|
17
|
+
"""Require an absent OCI-layout destination below an existing regular directory.
|
|
18
|
+
|
|
19
|
+
Tools that write `oci:` layouts neither create missing parents nor refuse an
|
|
20
|
+
existing layout, so every adapter that exports one applies the same
|
|
21
|
+
fail-closed preparation before the tool runs.
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
layout_path.lstat()
|
|
25
|
+
except FileNotFoundError:
|
|
26
|
+
pass
|
|
27
|
+
except OSError as exc:
|
|
28
|
+
raise OperationalError(
|
|
29
|
+
f"Unable to inspect output layout {layout_path}"
|
|
30
|
+
) from exc
|
|
31
|
+
else:
|
|
32
|
+
raise InvalidInvocationError(f"Output layout already exists: {layout_path}")
|
|
33
|
+
try:
|
|
34
|
+
layout_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
35
|
+
if not layout_path.parent.is_dir() or layout_path.parent.is_symlink():
|
|
36
|
+
raise InvalidInvocationError(
|
|
37
|
+
f"Output layout parent is not a regular directory: {layout_path.parent}"
|
|
38
|
+
)
|
|
39
|
+
except OSError as exc:
|
|
40
|
+
raise OperationalError(
|
|
41
|
+
f"Unable to create output layout parent {layout_path.parent}"
|
|
42
|
+
) from exc
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Runner(Protocol):
|
|
46
|
+
"""Narrow process runner boundary used by tool adapters."""
|
|
47
|
+
|
|
48
|
+
def run(self, request: CommandRequest) -> ProcessResult:
|
|
49
|
+
"""Execute one fully specified external process."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ToolAdapter:
|
|
54
|
+
"""Execute one immutable resolved tool through the central runner."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
tool: ResolvedTool,
|
|
60
|
+
runner: Runner,
|
|
61
|
+
environment: Mapping[str, str],
|
|
62
|
+
log_directory: Path,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Bind the adapter to a run-owned environment and log directory."""
|
|
65
|
+
self._tool = tool
|
|
66
|
+
self._runner = runner
|
|
67
|
+
self._environment = dict(environment)
|
|
68
|
+
self._log_directory = log_directory
|
|
69
|
+
self._sequence = 0
|
|
70
|
+
|
|
71
|
+
def _run(
|
|
72
|
+
self,
|
|
73
|
+
arguments: Sequence[str],
|
|
74
|
+
*,
|
|
75
|
+
operation: OperationKind = OperationKind.READ,
|
|
76
|
+
timeout_seconds: float,
|
|
77
|
+
retries: int = 0,
|
|
78
|
+
extra_environment: Mapping[str, str] | None = None,
|
|
79
|
+
secret_values: tuple[str, ...] = (),
|
|
80
|
+
secret_paths: tuple[Path, ...] = (),
|
|
81
|
+
cwd: Path | None = None,
|
|
82
|
+
stdout_artifact: Path | None = None,
|
|
83
|
+
max_artifact_bytes: int = 128 * 1024 * 1024,
|
|
84
|
+
) -> ProcessResult:
|
|
85
|
+
self._tool.assert_unchanged()
|
|
86
|
+
environment = dict(self._environment)
|
|
87
|
+
if extra_environment is not None:
|
|
88
|
+
environment.update(extra_environment)
|
|
89
|
+
self._sequence += 1
|
|
90
|
+
return self._runner.run(
|
|
91
|
+
CommandRequest(
|
|
92
|
+
argv=(str(self._tool.path), *arguments),
|
|
93
|
+
environment=environment,
|
|
94
|
+
timeout_seconds=timeout_seconds,
|
|
95
|
+
operation=operation,
|
|
96
|
+
retries=retries,
|
|
97
|
+
log_path=(
|
|
98
|
+
self._log_directory
|
|
99
|
+
/ f"{self._tool.name.value}-{self._sequence:04d}.json"
|
|
100
|
+
),
|
|
101
|
+
secret_values=secret_values,
|
|
102
|
+
secret_paths=secret_paths,
|
|
103
|
+
cwd=cwd,
|
|
104
|
+
stdout_artifact=stdout_artifact,
|
|
105
|
+
max_artifact_bytes=max_artifact_bytes,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Rootless Buildah OCI-layout adapter."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from conclear.adapters.base import ToolAdapter, prepare_new_layout_path
|
|
7
|
+
from conclear.oci import OCIGraph, validate_layout
|
|
8
|
+
from conclear.parsing import json_value, object_value
|
|
9
|
+
from conclear.process import OperationKind
|
|
10
|
+
from conclear.values import Platform
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class BuildObservation:
|
|
15
|
+
"""Observed result of one platform build."""
|
|
16
|
+
|
|
17
|
+
image_name: str
|
|
18
|
+
layout_path: Path
|
|
19
|
+
graph: OCIGraph
|
|
20
|
+
build_arguments: tuple[tuple[str, str], ...]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BuildahAdapter(ToolAdapter):
|
|
24
|
+
"""Build one platform using run-specific containers storage."""
|
|
25
|
+
|
|
26
|
+
def info(self, *, root: Path, runroot: Path) -> dict[str, object]:
|
|
27
|
+
"""Validate access to one isolated rootless Buildah storage."""
|
|
28
|
+
output = self._run(
|
|
29
|
+
(
|
|
30
|
+
"--root",
|
|
31
|
+
str(root),
|
|
32
|
+
"--runroot",
|
|
33
|
+
str(runroot),
|
|
34
|
+
"info",
|
|
35
|
+
"--format",
|
|
36
|
+
"{{json .}}",
|
|
37
|
+
),
|
|
38
|
+
timeout_seconds=120,
|
|
39
|
+
).stdout
|
|
40
|
+
return object_value(
|
|
41
|
+
json_value(output, label="Buildah info"), label="Buildah info"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def build(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
root: Path,
|
|
48
|
+
runroot: Path,
|
|
49
|
+
containerfile: Path,
|
|
50
|
+
context: Path,
|
|
51
|
+
platform: Platform,
|
|
52
|
+
image_name: str,
|
|
53
|
+
layout_path: Path,
|
|
54
|
+
layout_reference: str,
|
|
55
|
+
source_epoch: int,
|
|
56
|
+
build_arguments: dict[str, str],
|
|
57
|
+
auth_file: Path | None,
|
|
58
|
+
) -> BuildObservation:
|
|
59
|
+
"""Build and export a digest-preserving OCI image layout."""
|
|
60
|
+
prepare_new_layout_path(layout_path)
|
|
61
|
+
arguments: list[str] = [
|
|
62
|
+
"--root",
|
|
63
|
+
str(root),
|
|
64
|
+
"--runroot",
|
|
65
|
+
str(runroot),
|
|
66
|
+
"build",
|
|
67
|
+
"--format",
|
|
68
|
+
"oci",
|
|
69
|
+
"--platform",
|
|
70
|
+
str(platform),
|
|
71
|
+
"--source-date-epoch",
|
|
72
|
+
str(source_epoch),
|
|
73
|
+
"--rewrite-timestamp",
|
|
74
|
+
"--pull=always",
|
|
75
|
+
# Labels describe this image, never its base (IG0431). Annotation
|
|
76
|
+
# inheritance stays on: Buildah writes the base annotations through it.
|
|
77
|
+
"--inherit-labels=false",
|
|
78
|
+
"--file",
|
|
79
|
+
str(containerfile),
|
|
80
|
+
"--tag",
|
|
81
|
+
image_name,
|
|
82
|
+
]
|
|
83
|
+
if auth_file is not None:
|
|
84
|
+
arguments.extend(("--authfile", str(auth_file)))
|
|
85
|
+
for name, value in sorted(build_arguments.items()):
|
|
86
|
+
arguments.extend(("--build-arg", f"{name}={value}"))
|
|
87
|
+
arguments.append(str(context))
|
|
88
|
+
self._run(
|
|
89
|
+
arguments,
|
|
90
|
+
timeout_seconds=3600,
|
|
91
|
+
operation=OperationKind.WRITE,
|
|
92
|
+
secret_paths=(() if auth_file is None else (auth_file,)),
|
|
93
|
+
)
|
|
94
|
+
self._run(
|
|
95
|
+
(
|
|
96
|
+
"--root",
|
|
97
|
+
str(root),
|
|
98
|
+
"--runroot",
|
|
99
|
+
str(runroot),
|
|
100
|
+
"push",
|
|
101
|
+
"--format",
|
|
102
|
+
"oci",
|
|
103
|
+
image_name,
|
|
104
|
+
f"oci:{layout_path}:{layout_reference}",
|
|
105
|
+
),
|
|
106
|
+
timeout_seconds=1800,
|
|
107
|
+
operation=OperationKind.WRITE,
|
|
108
|
+
)
|
|
109
|
+
graph = validate_layout(layout_path, reference=layout_reference)
|
|
110
|
+
return BuildObservation(
|
|
111
|
+
image_name=image_name,
|
|
112
|
+
layout_path=layout_path,
|
|
113
|
+
graph=graph,
|
|
114
|
+
build_arguments=tuple(sorted(build_arguments.items())),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def remove_storage(self, *, root: Path, runroot: Path) -> None:
|
|
118
|
+
"""Remove every image in one run-owned Buildah storage root."""
|
|
119
|
+
self._run(
|
|
120
|
+
("--root", str(root), "--runroot", str(runroot), "rm", "--all"),
|
|
121
|
+
timeout_seconds=300,
|
|
122
|
+
operation=OperationKind.WRITE,
|
|
123
|
+
)
|
|
124
|
+
self._run(
|
|
125
|
+
("--root", str(root), "--runroot", str(runroot), "rmi", "--all"),
|
|
126
|
+
timeout_seconds=300,
|
|
127
|
+
operation=OperationKind.WRITE,
|
|
128
|
+
)
|
conclear/adapters/ci.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Typed observations from supported CI provider environments."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
7
|
+
|
|
8
|
+
from conclear.errors import InvalidInvocationError
|
|
9
|
+
from conclear.values import validate_source_revision
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class CIContextAbsent:
|
|
14
|
+
"""No supported CI provider marker was observed."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class CIContextInvalid:
|
|
19
|
+
"""A provider marker was observed without a usable complete context."""
|
|
20
|
+
|
|
21
|
+
provider: str | None
|
|
22
|
+
diagnostic: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class ObservedCIContext:
|
|
27
|
+
"""Narrow provider metadata observed from the current process environment."""
|
|
28
|
+
|
|
29
|
+
provider: str
|
|
30
|
+
server: str
|
|
31
|
+
repository: str
|
|
32
|
+
revision: str
|
|
33
|
+
run_id: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
type CIContextObservation = CIContextAbsent | CIContextInvalid | ObservedCIContext
|
|
37
|
+
|
|
38
|
+
_REPOSITORY_COMPONENT = re.compile(r"^[A-Za-z0-9._~-]+$")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class _ProviderSpec:
|
|
43
|
+
provider: str
|
|
44
|
+
server_variable: str
|
|
45
|
+
repository_variable: str
|
|
46
|
+
revision_variable: str
|
|
47
|
+
run_variable: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_PROVIDERS = {
|
|
51
|
+
"forgejo-actions": _ProviderSpec(
|
|
52
|
+
"forgejo-actions",
|
|
53
|
+
"FORGEJO_SERVER_URL",
|
|
54
|
+
"FORGEJO_REPOSITORY",
|
|
55
|
+
"FORGEJO_SHA",
|
|
56
|
+
"FORGEJO_RUN_ID",
|
|
57
|
+
),
|
|
58
|
+
# Gitea's native gitea.* values are expression contexts; its documented
|
|
59
|
+
# process environment retains GITHUB_* for these identity fields.
|
|
60
|
+
"gitea-actions": _ProviderSpec(
|
|
61
|
+
"gitea-actions",
|
|
62
|
+
"GITHUB_SERVER_URL",
|
|
63
|
+
"GITHUB_REPOSITORY",
|
|
64
|
+
"GITHUB_SHA",
|
|
65
|
+
"GITHUB_RUN_ID",
|
|
66
|
+
),
|
|
67
|
+
"github-actions": _ProviderSpec(
|
|
68
|
+
"github-actions",
|
|
69
|
+
"GITHUB_SERVER_URL",
|
|
70
|
+
"GITHUB_REPOSITORY",
|
|
71
|
+
"GITHUB_SHA",
|
|
72
|
+
"GITHUB_RUN_ID",
|
|
73
|
+
),
|
|
74
|
+
"gitlab-ci": _ProviderSpec(
|
|
75
|
+
"gitlab-ci",
|
|
76
|
+
"CI_SERVER_URL",
|
|
77
|
+
"CI_PROJECT_PATH",
|
|
78
|
+
"CI_COMMIT_SHA",
|
|
79
|
+
"CI_PIPELINE_ID",
|
|
80
|
+
),
|
|
81
|
+
"woodpecker-ci": _ProviderSpec(
|
|
82
|
+
"woodpecker-ci",
|
|
83
|
+
"CI_FORGE_URL",
|
|
84
|
+
"CI_REPO",
|
|
85
|
+
"CI_COMMIT_SHA",
|
|
86
|
+
"CI_PIPELINE_NUMBER",
|
|
87
|
+
),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def observe_ci_context(environment: Mapping[str, str]) -> CIContextObservation:
|
|
92
|
+
"""Observe and validate a supported provider's ordinary environment metadata."""
|
|
93
|
+
providers = list(_detected_providers(environment))
|
|
94
|
+
github_compatible = (
|
|
95
|
+
environment.get("GITHUB_ACTIONS") == "true"
|
|
96
|
+
and environment.get("FORGEJO_ACTIONS") != "true"
|
|
97
|
+
and environment.get("GITEA_ACTIONS") != "true"
|
|
98
|
+
)
|
|
99
|
+
if github_compatible:
|
|
100
|
+
if not _is_github_environment(environment):
|
|
101
|
+
return CIContextInvalid(
|
|
102
|
+
None,
|
|
103
|
+
"GitHub-compatible CI variables lack a provider-specific marker",
|
|
104
|
+
)
|
|
105
|
+
providers.append("github-actions")
|
|
106
|
+
if not providers:
|
|
107
|
+
return CIContextAbsent()
|
|
108
|
+
if len(providers) != 1:
|
|
109
|
+
return CIContextInvalid(None, "Multiple CI provider markers were observed")
|
|
110
|
+
provider = providers[0]
|
|
111
|
+
spec = _PROVIDERS[provider]
|
|
112
|
+
try:
|
|
113
|
+
return ObservedCIContext(
|
|
114
|
+
provider=provider,
|
|
115
|
+
server=_https_url(environment.get(spec.server_variable), "CI server"),
|
|
116
|
+
repository=_repository(
|
|
117
|
+
environment.get(spec.repository_variable), "CI repository"
|
|
118
|
+
),
|
|
119
|
+
revision=validate_source_revision(
|
|
120
|
+
_text(environment.get(spec.revision_variable), "CI revision")
|
|
121
|
+
),
|
|
122
|
+
run_id=_text(environment.get(spec.run_variable), "CI run identifier"),
|
|
123
|
+
)
|
|
124
|
+
except (InvalidInvocationError, ValueError) as exc:
|
|
125
|
+
return CIContextInvalid(provider, str(exc))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _detected_providers(environment: Mapping[str, str]) -> tuple[str, ...]:
|
|
129
|
+
providers: list[str] = []
|
|
130
|
+
forgejo = environment.get("FORGEJO_ACTIONS") == "true"
|
|
131
|
+
gitea = environment.get("GITEA_ACTIONS") == "true"
|
|
132
|
+
if forgejo:
|
|
133
|
+
providers.append("forgejo-actions")
|
|
134
|
+
if gitea:
|
|
135
|
+
providers.append("gitea-actions")
|
|
136
|
+
if environment.get("GITLAB_CI") == "true":
|
|
137
|
+
providers.append("gitlab-ci")
|
|
138
|
+
if (
|
|
139
|
+
environment.get("CI") == "woodpecker"
|
|
140
|
+
or environment.get("CI_SYSTEM_NAME") == "woodpecker"
|
|
141
|
+
):
|
|
142
|
+
providers.append("woodpecker-ci")
|
|
143
|
+
return tuple(providers)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _is_github_environment(environment: Mapping[str, str]) -> bool:
|
|
147
|
+
server = environment.get("GITHUB_SERVER_URL", "").rstrip("/")
|
|
148
|
+
api = environment.get("GITHUB_API_URL", "").rstrip("/")
|
|
149
|
+
if server == "https://github.com":
|
|
150
|
+
return api == "https://api.github.com"
|
|
151
|
+
return bool(server) and api == f"{server}/api/v3"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _text(value: str | None, label: str) -> str:
|
|
155
|
+
if (
|
|
156
|
+
value is None
|
|
157
|
+
or not value
|
|
158
|
+
or len(value) > 1024
|
|
159
|
+
or any(character.isspace() for character in value)
|
|
160
|
+
):
|
|
161
|
+
raise ValueError(f"{label} is missing or malformed")
|
|
162
|
+
return value
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _repository(value: str | None, label: str) -> str:
|
|
166
|
+
result = _text(value, label)
|
|
167
|
+
components = result.split("/")
|
|
168
|
+
if (
|
|
169
|
+
len(components) < 2
|
|
170
|
+
or result.endswith(".git")
|
|
171
|
+
or any(
|
|
172
|
+
component in {"", ".", ".."}
|
|
173
|
+
or _REPOSITORY_COMPONENT.fullmatch(component) is None
|
|
174
|
+
for component in components
|
|
175
|
+
)
|
|
176
|
+
):
|
|
177
|
+
raise ValueError(f"{label} is malformed")
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _https_url(value: str | None, label: str) -> str:
|
|
182
|
+
result = _text(value, label)
|
|
183
|
+
try:
|
|
184
|
+
parsed = urlsplit(result)
|
|
185
|
+
port = parsed.port
|
|
186
|
+
except ValueError as exc:
|
|
187
|
+
raise ValueError(f"{label} is malformed") from exc
|
|
188
|
+
if (
|
|
189
|
+
parsed.scheme != "https"
|
|
190
|
+
or parsed.hostname is None
|
|
191
|
+
or parsed.username is not None
|
|
192
|
+
or parsed.password is not None
|
|
193
|
+
or parsed.query
|
|
194
|
+
or parsed.fragment
|
|
195
|
+
):
|
|
196
|
+
raise ValueError(f"{label} must be a credential-free HTTPS URL")
|
|
197
|
+
authority = parsed.hostname.lower() + ("" if port is None else f":{port}")
|
|
198
|
+
return urlunsplit(("https", authority, parsed.path.rstrip("/"), "", ""))
|