Archived
KofTwentyTwo/ci-policy: ci / fork-guard (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-nix-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-docker-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-maven-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / manifests-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / nix-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / python-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / not-enrolled (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / content-validate-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / standards-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-matrix-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-15 (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / verify (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / secrets (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / gate (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: ci / ci-result (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: native / fork-deny (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-15 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-16 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-17 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-18 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-19 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / native-result (pull_request) Canceled after 6s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
153 lines
7.5 KiB
Python
153 lines
7.5 KiB
Python
"""Run the planned native commands of one target in the ordinary sandbox.
|
|
|
|
Commands are token lists started without a shell, with a minimal environment that
|
|
cannot carry release or runner credentials into source-controlled build code.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
from collections.abc import Callable, Mapping, Sequence
|
|
from pathlib import Path
|
|
|
|
from platform_ci.build_tools import BUILD_TOOLS, POWERSHELL_LAUNCHER, POWERSHELL_PROGRAM_SUFFIX
|
|
from platform_ci.maven_settings import install_public_central_settings
|
|
from platform_ci.plan import Target
|
|
from platform_ci.source_helpers import verify_source_helpers
|
|
from platform_ci.source_tools import helper_invocation, subcommand_named
|
|
from platform_ci.workspace import PipelineError, resolve_component_directory
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
# Host runners (Windows, macOS) need their own system variables to start a tool at all; none of them is a credential.
|
|
INHERITED_VARIABLES = (
|
|
"PATH",
|
|
"HOME",
|
|
"LANG",
|
|
"LC_ALL",
|
|
"TMPDIR",
|
|
"JAVA_HOME",
|
|
"MAVEN_OPTS",
|
|
"MAVEN_CONFIG",
|
|
"CI",
|
|
"SYSTEMROOT",
|
|
"USERPROFILE",
|
|
"TEMP",
|
|
"TMP",
|
|
"PATHEXT",
|
|
"COMSPEC",
|
|
"APPDATA",
|
|
"LOCALAPPDATA",
|
|
"PROGRAMFILES",
|
|
"PROGRAMDATA",
|
|
"DEVELOPER_DIR",
|
|
)
|
|
|
|
RUN_IDENTITY_VARIABLES = ("CI_RUN_ID", "CI_RUN_ATTEMPT")
|
|
"""Passed from the workflow's own context, only to a target whose environment carries the event facts; deliberately outside the target, so the build-input identity
|
|
(and with it reuse and the publisher's equivalence proof) does not change with every run."""
|
|
|
|
CommandRunner = Callable[[Sequence[str], Path, Mapping[str, str]], int]
|
|
|
|
|
|
class ExecutionError(PipelineError):
|
|
"""Raised when a planned command cannot be started at all."""
|
|
|
|
|
|
def sandbox_environment(parent_environment: Mapping[str, str]) -> dict[str, str]:
|
|
"""Keep only the allow-listed variables, so tokens injected by the runner never reach build code."""
|
|
return {name: value for name, value in parent_environment.items() if name in INHERITED_VARIABLES}
|
|
|
|
|
|
def git_trust_environment(directory: Path) -> dict[str, str]:
|
|
"""Return the environment that lets subprocess Git inspect exactly one checkout, without touching any host or user Git state.
|
|
|
|
The checkout is made by the runner's builtin checkout, whose owner can differ from the user the job runs as, so Git refuses it as
|
|
`dubious ownership`. Command-scope configuration through `GIT_CONFIG_COUNT` is honored by Git for `safe.directory` and exists only in the
|
|
environment of the one process started here: no global or system file is written, nothing is a wildcard, and only this exact resolved path
|
|
is trusted (a parent, a subdirectory or a sibling is not). `safe.directory` matches by path text, so forward slashes are used on every host.
|
|
"""
|
|
return {
|
|
"GIT_CONFIG_COUNT": "1",
|
|
"GIT_CONFIG_KEY_0": "safe.directory",
|
|
"GIT_CONFIG_VALUE_0": directory.resolve().as_posix(),
|
|
}
|
|
|
|
|
|
def run_process(command: Sequence[str], working_directory: Path, environment: Mapping[str, str]) -> int:
|
|
"""Start one tokenized command without a shell and return its exit code."""
|
|
try:
|
|
return subprocess.run(list(command), cwd=working_directory, env=dict(environment), shell=False, check=False).returncode # noqa: S603
|
|
except OSError as error:
|
|
raise ExecutionError(f"cannot start {command[0]!r}: {error}") from error
|
|
|
|
|
|
def launch_command(command: Sequence[str]) -> list[str]:
|
|
"""Return what to start for one declared program: the program itself, or a `.ps1` file behind the fixed PowerShell launcher.
|
|
|
|
The launcher is `pwsh -NoProfile -NonInteractive -File <program> <arguments>` on any platform: no shell, no execution-policy override, nothing downloaded.
|
|
A runtime without `pwsh` fails as the ordinary `cannot start` error.
|
|
"""
|
|
if command[0].lower().endswith(POWERSHELL_PROGRAM_SUFFIX):
|
|
return [*POWERSHELL_LAUNCHER, *command]
|
|
return list(command)
|
|
|
|
|
|
def _prepare_command(target: Target, planned_command: list[str], working_directory: Path, environment: dict[str, str]) -> tuple[list[str], dict[str, str]]:
|
|
"""Return the command to start and its environment: helper and program checks run first, so nothing outside the component directory is ever started."""
|
|
command, command_environment = planned_command, environment
|
|
if BUILD_TOOLS[target["build_tool"]].helper_subcommands:
|
|
command, helper_environment = helper_invocation(planned_command, working_directory)
|
|
command_environment = environment | helper_environment
|
|
subcommand = subcommand_named(planned_command[0])
|
|
if subcommand is not None and subcommand.inspects_checkout_with_git:
|
|
command_environment |= git_trust_environment(working_directory)
|
|
if BUILD_TOOLS[target["build_tool"]].subcommands:
|
|
program = (working_directory / command[1]).resolve()
|
|
if not program.is_relative_to(working_directory) or not program.is_file():
|
|
raise ExecutionError(f"{command[1]!r} is not a file inside the component directory")
|
|
verify_source_helpers(program.parent)
|
|
if BUILD_TOOLS[target["build_tool"]].program_in_arguments:
|
|
program = (working_directory / command[0]).resolve()
|
|
if not program.is_relative_to(working_directory) or not program.is_file():
|
|
raise ExecutionError(f"{command[0]!r} is not a file inside the component directory")
|
|
command = launch_command(command)
|
|
return command, command_environment
|
|
|
|
|
|
def run_target_commands(
|
|
target: Target,
|
|
source_directory: Path,
|
|
run_command: CommandRunner = run_process,
|
|
parent_environment: Mapping[str, str] = os.environ,
|
|
) -> bool:
|
|
"""Run the target's commands in order, stopping at the first failure; return whether all succeeded.
|
|
|
|
The environment is the allow-listed runner variables plus the target's own fixed, non-secret tool variables. A Maven
|
|
target first gets the credential-free public-repository settings in its home directory.
|
|
"""
|
|
working_directory = resolve_component_directory(source_directory, target["directory"])
|
|
environment = sandbox_environment(parent_environment) | target["environment"]
|
|
if "CI_EVENT_NAME" in target["environment"]:
|
|
# a script that opted in to event facts also gets the run identity the workflow supplies, and nothing else from the parent: two numeric values
|
|
for name in RUN_IDENTITY_VARIABLES:
|
|
value = parent_environment.get(name, "")
|
|
if not value.isascii() or not value.isdigit() or int(value) <= 0:
|
|
raise ExecutionError(f"{name} must be the workflow's positive run number for a target that reads event facts")
|
|
environment[name] = value
|
|
if BUILD_TOOLS[target["build_tool"]].inspects_checkout_with_git:
|
|
environment |= git_trust_environment(working_directory)
|
|
if BUILD_TOOLS[target["build_tool"]].public_central_settings or (BUILD_TOOLS[target["build_tool"]].toolchain_choices and target["toolchain"] == "java"):
|
|
home = environment.get("HOME", "")
|
|
if not home:
|
|
raise ExecutionError("HOME is not set, so the Maven settings location is unknown")
|
|
LOG.info("verify %s: Maven settings written to %s", target["name"], install_public_central_settings(Path(home)))
|
|
for planned_command in target["commands"]:
|
|
command, command_environment = _prepare_command(target, planned_command, working_directory, environment)
|
|
LOG.info("verify %s: running %s", target["name"], command)
|
|
exit_code = run_command(command, working_directory, command_environment)
|
|
if exit_code != 0:
|
|
LOG.error("verify %s: %s exited with %d", target["name"], command[0], exit_code)
|
|
return False
|
|
return True
|