feat(builder): query CI via project status script

When BUILD_VENUE=ci, init confirms tea/glab/gh/curl (or a custom
command) and writes .pouch/builder/ci-status.sh. wait_ci.sh only
polls that script and no longer guesses forge CLIs.
This commit is contained in:
2026-10-15 09:56:51 +08:00
parent 0c9a530390
commit 9ba9b3da0f
17 changed files with 743 additions and 595 deletions
+49 -52
View File
@@ -551,8 +551,8 @@ def configured_value(
return os.environ.get(key, "") or env_values.get(key, "") or (make_values or {}).get(key, "")
def resolve_copy_script(project: Path, script: str) -> tuple[Path | None, str | None]:
"""Resolve COPY_SCRIPT without allowing arbitrary project-relative paths."""
def resolve_builder_local_script(project: Path, script: str) -> tuple[Path | None, str | None]:
"""Resolve a project script; relative paths must stay under .pouch/builder/."""
configured = Path(script).expanduser()
if configured.is_absolute():
return configured, None
@@ -561,10 +561,17 @@ def resolve_copy_script(project: Path, script: str) -> tuple[Path | None, str |
allowed_root = (project / ".pouch" / "builder").resolve()
resolved.relative_to(allowed_root)
except ValueError:
return None, "relative COPY_SCRIPT must be under .pouch/builder/"
return None, "relative script must be under .pouch/builder/"
return resolved, None
def resolve_copy_script(project: Path, script: str) -> tuple[Path | None, str | None]:
path, error = resolve_builder_local_script(project, script)
if error:
return None, "relative COPY_SCRIPT must be under .pouch/builder/"
return path, None
def check_copy_script(
report: Report,
project: Path,
@@ -591,6 +598,9 @@ def check_copy_script(
def read_venue(project: Path) -> str:
env = (os.environ.get("BUILD_VENUE") or "").strip()
if env in ("local", "ci"):
return env
result = run_make(project, "venue")
if not has_no_rule(result):
echoed = parse_dry_echo(result)
@@ -604,34 +614,35 @@ def read_venue(project: Path) -> str:
return raw or "local"
def origin_forge(project: Path) -> str:
result = subprocess.run(
["git", "-C", str(project), "remote", "get-url", "origin"],
capture_output=True,
text=True,
check=False,
def check_ci_status_script(
report: Report,
project: Path,
env_values: dict[str, str],
make_values: dict[str, str],
) -> None:
if read_venue(project) != "ci":
report.add(SKIP, 15, "CI_STATUS_SCRIPT 可执行", "BUILD_VENUE is not ci")
return
script = configured_value("CI_STATUS_SCRIPT", env_values, make_values)
problems = []
if not script:
problems.append("BUILD_VENUE=ci requires CI_STATUS_SCRIPT")
path = None
resolve_error = None
else:
path, resolve_error = resolve_builder_local_script(project, script)
if resolve_error:
problems.append(resolve_error)
elif path is not None and not path.is_file():
problems.append("CI_STATUS_SCRIPT does not point to a file")
elif path is not None and not os.access(path, os.X_OK):
problems.append("CI_STATUS_SCRIPT is not executable")
report.add(
FAIL if problems else PASS,
15,
"CI_STATUS_SCRIPT 可执行",
"\n".join(problems) if problems else f"ci status script: {script}",
)
url = (result.stdout or "").strip()
configured = env_file_values(project).get("BUILDER_CI_FORGE", "").strip().lower()
forced = (os.environ.get("BUILDER_CI_FORGE") or configured).strip().lower()
if forced in ("github", "gitlab", "gitea"):
return forced
if "github.com" in url:
return "github"
host_match = re.search(r"(?:https?://|ssh://git@|git@)([^/:]+)", url)
host = host_match.group(1).lower() if host_match else ""
builder_keys = env_file_keys(project)
if (
"gitea" in host
or "GITEA_TOKEN" in builder_keys
or "GITEA_API_URL" in builder_keys
or os.environ.get("GITEA_TOKEN")
or os.environ.get("GITEA_API_URL")
):
return "gitea"
if url:
return "gitlab"
return ""
def check_ready_toolchain(
@@ -655,28 +666,7 @@ def check_ready_toolchain(
lines.append(
f"docker: {'found' if docker_ok else 'optional for inspect; obtain fails without it'}"
)
forge = origin_forge(project) if project is not None else ""
if forge == "github":
gh_ok = shutil.which("gh") is not None
lines.append(f"gh: {'found' if gh_ok else 'MISSING (blocks ci venue)'}")
if not gh_ok:
problems.append("install gh and authenticate for this GitHub repo")
elif forge == "gitlab":
glab_ok = shutil.which("glab") is not None
lines.append(f"glab: {'found' if glab_ok else 'MISSING (blocks ci venue)'}")
if not glab_ok:
problems.append("install glab and authenticate for this GitLab project")
elif forge == "gitea":
curl_ok = shutil.which("curl") is not None
lines.append(f"curl: {'found' if curl_ok else 'MISSING (blocks ci venue)'}")
lines.append("Gitea API: /api/v1; GITEA_TOKEN optional for public repositories")
if not curl_ok:
problems.append("install curl to read Gitea Actions for this repository")
else:
glab_ok = shutil.which("glab") is not None
lines.append(f"glab: {'found' if glab_ok else 'MISSING (blocks ci venue)'}")
if not glab_ok:
problems.append("install glab and authenticate for this GitLab project")
lines.append("CI status: project CI_STATUS_SCRIPT (tea/glab/gh/curl)")
elif docker_project:
docker_ok = shutil.which("docker") is not None
lines.append(f"docker: {'found' if docker_ok else 'MISSING (blocks docker track)'}")
@@ -867,6 +857,7 @@ def main(argv: list[str] | None = None) -> int:
check_ready_env_keys(
report, project, deb_project=deb_project, docker_project=docker_project
)
check_ci_status_script(report, project, env_file_values(project), makefile_values(project) if (project / BUILDER_MAKEFILE).is_file() else {})
print()
print(f"RESULT: FAILED ({report.failures} check(s) failed)")
return 1
@@ -963,6 +954,12 @@ def main(argv: list[str] | None = None) -> int:
docker_project=docker_project,
copy_project=copy_project,
)
check_ci_status_script(
report,
project,
env_file_values(project),
makefile_values(project),
)
total_fail = report.failures
print()