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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user