e358b9b92f
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 8s
CI / Detect changes (pull_request) Successful in 16s
CI / Python Lint & Test (pull_request) Successful in 1m2s
E2E API Smoke Test / detect-changes (pull_request) Successful in 1m12s
E2E Chat / detect-changes (pull_request) Successful in 18s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Has been skipped
Harness Replays / detect-changes (pull_request) Successful in 20s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 20s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 22s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 6s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 9s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 11s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Successful in 1m50s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m44s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 6s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m42s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m40s
lint-mask-pr-atomicity / lint-mask-pr-atomicity (pull_request) Successful in 2m2s
gate-check-v3 / gate-check (pull_request) Successful in 16s
qa-review / approved (pull_request) Successful in 12s
sop-checklist / review-refire (pull_request) Has been skipped
sop-checklist / na-declarations (pull_request) N/A: (none)
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 2m0s
security-review / approved (pull_request) Successful in 6s
sop-checklist / all-items-acked (pull_request) Successful in 5s
sop-tier-check / tier-check (pull_request) Successful in 6s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 1m21s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m31s
CI / Platform (Go) (pull_request) Successful in 4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 3s
CI / Canvas (Next.js) (pull_request) Successful in 23s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 10s
E2E Chat / E2E Chat (pull_request) Successful in 12s
CI / all-required (pull_request) Successful in 7m47s
Harness Replays / Harness Replays (pull_request) Successful in 16s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m55s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1m48s
audit-force-merge / audit (pull_request) Successful in 10s
175 lines
5.0 KiB
Python
175 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Shared path-filter helper for Gitea Actions workflows.
|
|
|
|
Computes changed files against the PR base SHA or push-before SHA and writes
|
|
boolean outputs to GITHUB_OUTPUT. If the diff base is missing or untrusted, the
|
|
helper fails open by setting every output in the selected profile to true.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
PROFILES: dict[str, dict[str, str]] = {
|
|
"ci": {
|
|
"platform": r"^workspace-server/",
|
|
"canvas": r"^canvas/",
|
|
"python": r"^workspace/",
|
|
"scripts": r"^tests/e2e/|^scripts/|^infra/scripts/",
|
|
},
|
|
"handlers-postgres": {
|
|
"handlers": (
|
|
r"^workspace-server/internal/handlers/"
|
|
r"|^workspace-server/internal/wsauth/"
|
|
r"|^workspace-server/migrations/"
|
|
r"|^\.gitea/workflows/handlers-postgres-integration\.yml$"
|
|
),
|
|
},
|
|
"e2e-api": {
|
|
"api": r"^workspace-server/|^tests/e2e/|^\.gitea/workflows/e2e-api\.yml$",
|
|
},
|
|
}
|
|
|
|
|
|
def classify(profile: str, paths: list[str]) -> dict[str, bool]:
|
|
patterns = PROFILES[profile]
|
|
return {
|
|
name: any(re.search(pattern, path) for path in paths)
|
|
for name, pattern in patterns.items()
|
|
}
|
|
|
|
|
|
def all_true(profile: str) -> dict[str, bool]:
|
|
return {name: True for name in PROFILES[profile]}
|
|
|
|
|
|
def resolve_base(event_name: str, pr_base_sha: str, push_before: str) -> str:
|
|
if event_name == "pull_request" and pr_base_sha:
|
|
return pr_base_sha
|
|
return push_before
|
|
|
|
|
|
def is_zero_sha(value: str) -> bool:
|
|
return not value or bool(re.fullmatch(r"0+", value))
|
|
|
|
|
|
def run_git(args: list[str], *, timeout: int = 30) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
["git", *args],
|
|
check=False,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
timeout=timeout,
|
|
)
|
|
|
|
|
|
def base_exists(base: str) -> bool:
|
|
return run_git(["cat-file", "-e", base]).returncode == 0
|
|
|
|
|
|
def fetch_base(base: str, base_ref: str) -> None:
|
|
# Gitea may reject fetching an arbitrary unadvertised SHA from a shallow
|
|
# PR checkout. Fetch the advertised base branch first, then fall back to
|
|
# the SHA for hosts that allow it.
|
|
if base_ref:
|
|
run_git(["fetch", "--depth=1", "origin", base_ref])
|
|
if not base_exists(base):
|
|
run_git(["fetch", "--depth=1", "origin", base])
|
|
|
|
|
|
def deepen_base_ref(base_ref: str) -> None:
|
|
if base_ref:
|
|
run_git(["fetch", "--deepen=200", "origin", base_ref], timeout=60)
|
|
|
|
|
|
def merge_base(base: str) -> str | None:
|
|
proc = run_git(["merge-base", base, "HEAD"])
|
|
if proc.returncode != 0:
|
|
return None
|
|
value = proc.stdout.strip()
|
|
return value or None
|
|
|
|
|
|
def changed_paths(base: str, *, use_merge_base: bool) -> list[str] | None:
|
|
compare_base = base
|
|
if use_merge_base:
|
|
compare_base = merge_base(base) or ""
|
|
if not compare_base:
|
|
return None
|
|
|
|
proc = run_git(["diff", "--name-only", compare_base, "HEAD"])
|
|
if proc.returncode != 0:
|
|
return None
|
|
return [line for line in proc.stdout.splitlines() if line]
|
|
|
|
|
|
def write_outputs(values: dict[str, bool], output_path: str | None) -> None:
|
|
lines = [f"{name}={'true' if value else 'false'}" for name, value in values.items()]
|
|
if output_path:
|
|
with Path(output_path).open("a", encoding="utf-8") as fh:
|
|
for line in lines:
|
|
fh.write(line + "\n")
|
|
else:
|
|
for line in lines:
|
|
print(line)
|
|
|
|
|
|
def detect(
|
|
profile: str,
|
|
event_name: str,
|
|
pr_base_sha: str,
|
|
push_before: str,
|
|
base_ref: str = "",
|
|
) -> dict[str, bool]:
|
|
base = resolve_base(event_name, pr_base_sha, push_before)
|
|
if is_zero_sha(base):
|
|
return all_true(profile)
|
|
|
|
if not base_exists(base):
|
|
fetch_base(base, base_ref)
|
|
if not base_exists(base):
|
|
return all_true(profile)
|
|
|
|
use_merge_base = event_name == "pull_request"
|
|
if use_merge_base and base_ref and merge_base(base) is None:
|
|
deepen_base_ref(base_ref)
|
|
|
|
paths = changed_paths(base, use_merge_base=use_merge_base)
|
|
if paths is None:
|
|
return all_true(profile)
|
|
return classify(profile, paths)
|
|
|
|
|
|
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--profile", required=True, choices=sorted(PROFILES))
|
|
parser.add_argument("--event-name", default=os.environ.get("GITHUB_EVENT_NAME", ""))
|
|
parser.add_argument("--pr-base-sha", default="")
|
|
parser.add_argument("--base-ref", default="")
|
|
parser.add_argument("--push-before", default=os.environ.get("GITHUB_EVENT_BEFORE", ""))
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
args = parse_args(argv)
|
|
values = detect(
|
|
args.profile,
|
|
args.event_name,
|
|
args.pr_base_sha,
|
|
args.push_before,
|
|
args.base_ref,
|
|
)
|
|
write_outputs(values, os.environ.get("GITHUB_OUTPUT"))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|