fix: make dev tools executable

This commit is contained in:
2026-07-20 20:55:57 +02:00
parent cb2301d709
commit 0af5a8b00f
3 changed files with 19 additions and 5 deletions
+4 -2
View File
@@ -3,7 +3,9 @@
This directory supplies the ready-made `python` and `dotnet` scripts. They are not reserved: This directory supplies the ready-made `python` and `dotnet` scripts. They are not reserved:
modify, replace, or remove them like any other script. Place other trusted executable install modify, replace, or remove them like any other script. Place other trusted executable install
scripts here and add the desired file names to `AGENTCI_INSTALL_SCRIPTS`. Compose mounts the scripts here and add the desired file names to `AGENTCI_INSTALL_SCRIPTS`. Compose mounts the
directory read-only at `/etc/agentci/install-scripts`. directory read-only at `/etc/agentci/install-scripts`. Executable files run directly; files without
executable mode run as POSIX shell scripts through `/bin/sh` so bind mounts do not depend on host
file-mode preservation.
Scripts run from the cloned repository with a sanitized environment. They receive: Scripts run from the cloned repository with a sanitized environment. They receive:
@@ -19,4 +21,4 @@ not persist into implementation turns.
The configured scripts run in list order after the repository is cloned (or an existing agent PR The configured scripts run in list order after the repository is cloned (or an existing agent PR
branch is synchronized) and before the first implementation turn for `implement`, `iterate`, and branch is synchronized) and before the first implementation turn for `implement`, `iterate`, and
`fix`. They do not rerun between implementation and review-revision turns within one job. Scripts `fix`. They do not rerun between implementation and review-revision turns within one job. Scripts
must be executable, idempotent, and must not expect AgentCI or Gitea credentials. must be idempotent and must not expect AgentCI or Gitea credentials.
+2 -3
View File
@@ -55,19 +55,18 @@ class DevelopmentEnvironment:
) from exc ) from exc
if resolved.parent != self.scripts_dir.resolve() or not resolved.is_file(): if resolved.parent != self.scripts_dir.resolve() or not resolved.is_file():
raise DevelopmentEnvironmentError(f"Install script {name!r} is not a regular file") raise DevelopmentEnvironmentError(f"Install script {name!r} is not a regular file")
if not os.access(resolved, os.X_OK):
raise DevelopmentEnvironmentError(f"Install script {name!r} is not executable")
return resolved return resolved
async def _run(self, name: str, script: Path, workspace: Path) -> None: async def _run(self, name: str, script: Path, workspace: Path) -> None:
started = monotonic() started = monotonic()
command = [str(script)] if os.access(script, os.X_OK) else ["/bin/sh", str(script)]
log.info( log.info(
"development install script started", "development install script started",
extra={"operation": "development.install", "script": name}, extra={"operation": "development.install", "script": name},
) )
try: try:
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
str(script), *command,
cwd=workspace, cwd=workspace,
env=self._environment(), env=self._environment(),
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
+13
View File
@@ -61,6 +61,19 @@ async def test_supplied_script_names_use_the_same_directory(tmp_path) -> None:
assert (workspace / "selected").read_text() == "python\n" assert (workspace / "selected").read_text() == "python\n"
async def test_runs_non_executable_shell_script_from_bind_mount(tmp_path) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
development = environment(tmp_path, ["mounted"])
mounted = development.scripts_dir / "mounted"
mounted.write_text("#!/bin/sh\nprintf 'mounted\\n' > selected\n")
mounted.chmod(0o644)
await development.prepare(workspace)
assert (workspace / "selected").read_text() == "mounted\n"
async def test_reports_script_failure_output(tmp_path) -> None: async def test_reports_script_failure_output(tmp_path) -> None:
workspace = tmp_path / "workspace" workspace = tmp_path / "workspace"
workspace.mkdir() workspace.mkdir()