mirror of
https://github.com/apache/nuttx.git
synced 2026-08-01 20:28:58 +00:00
Allow pull requests targeting master to declare same- and cross-repository dependencies. Parse declarations with a tested Python helper, apply exact dependency commits before the existing build matrix, and rerun heavy CI only when an edited description changes the dependency state. Keep fork builds read-only and use a trusted workflow_run to validate artifacts and post per-build dependency results. Document the supported declaration forms and operational limits. Assisted-by: Kiro:gpt-5.6-sol Signed-off-by: zhangning21 <zhangning21@xiaomi.com>
197 lines
6.4 KiB
Python
197 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
"""Parse ``depends-on:`` declarations from a NuttX pull request body.
|
|
|
|
Supported forms are a single reference, an inline list, or multiple declaration
|
|
lines. References may use ``owner/repo/pull/N`` or a full GitHub URL. Bullet-list
|
|
continuations are not supported.
|
|
|
|
Declarations must start a line with at most three spaces and must be outside
|
|
Markdown code blocks. Repositories are restricted to ``NUTTX_REPO`` and
|
|
``APPS_REPO``; PR numbers are positive JavaScript-safe integers.
|
|
|
|
CLI:
|
|
python3 depends_on.py # print result JSON
|
|
python3 depends_on.py --print-state # print state used by the edit gate
|
|
python3 depends_on.py --github-output # write workflow outputs and report
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
# Limit PR numbers to JavaScript's exact integer range because the comment
|
|
# workflow reads the report. The 19-digit regex limit also avoids huge int()
|
|
# conversions; it is not a GitHub business limit.
|
|
_MAX_SAFE_PR_NUMBER = 9007199254740991 # 2**53 - 1
|
|
_TOKEN_RE = re.compile(
|
|
r"^(?:https://github\.com/)?"
|
|
r"(?P<repo>[A-Za-z0-9._-]+/[A-Za-z0-9._-]+)/pull/(?P<num>[1-9][0-9]{0,18})$"
|
|
)
|
|
|
|
# CommonMark treats four leading spaces or a tab as indented code. Anchoring
|
|
# also excludes prose and prefixes such as "not-depends-on:".
|
|
_MARKER_RE = re.compile(r"^ {0,3}depends-on:[ \t]*(?P<rest>.*)$", re.IGNORECASE)
|
|
|
|
# CommonMark fences may be indented by at most three spaces.
|
|
_FENCE_RE = re.compile(r"^ {0,3}(?:```|~~~)")
|
|
|
|
|
|
def allowed_repos_from_env():
|
|
return (
|
|
os.environ.get("NUTTX_REPO", "apache/nuttx"),
|
|
os.environ.get("APPS_REPO", "apache/nuttx-apps"),
|
|
)
|
|
|
|
|
|
def _split_tokens(text):
|
|
# GitHub Markdown does not treat Unicode separators as declaration
|
|
# boundaries, so split tokens on ASCII separators only.
|
|
return [t for t in re.split(r"[ \t\r\n\[\],]+", text.strip()) if t]
|
|
|
|
|
|
def _lines(body):
|
|
"""Split on newline forms rendered by GitHub Markdown."""
|
|
return (body or "").replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
|
|
|
|
def has_declaration(body):
|
|
"""True if the body has a line-anchored depends-on: outside code fences."""
|
|
in_fence = False
|
|
for ln in _lines(body):
|
|
if _FENCE_RE.match(ln):
|
|
in_fence = not in_fence
|
|
continue
|
|
if not in_fence and _MARKER_RE.match(ln):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _declared_tokens(body):
|
|
"""Yield tokens from single-line declarations outside code fences."""
|
|
in_fence = False
|
|
for ln in _lines(body):
|
|
if _FENCE_RE.match(ln):
|
|
in_fence = not in_fence
|
|
continue
|
|
if in_fence:
|
|
continue
|
|
m = _MARKER_RE.match(ln)
|
|
if m:
|
|
for t in _split_tokens(m.group("rest")):
|
|
yield t
|
|
|
|
|
|
def parse_dependencies(body, allowed_repos=None, pr_number=None, head_sha=None):
|
|
"""Return the structured dependency result for ``body``."""
|
|
if allowed_repos is None:
|
|
allowed_repos = allowed_repos_from_env()
|
|
|
|
deps = []
|
|
warnings = []
|
|
seen = set()
|
|
for t in _declared_tokens(body):
|
|
m = _TOKEN_RE.match(t)
|
|
if not m:
|
|
continue # stray text on a declaration line; ignore
|
|
repo = m.group("repo")
|
|
num = int(m.group("num"))
|
|
if num > _MAX_SAFE_PR_NUMBER:
|
|
continue # beyond JS safe integer; comment workflow can't handle it
|
|
if repo not in allowed_repos:
|
|
warnings.append("Ignoring unsupported dependency repo: " + repo)
|
|
continue
|
|
key = (repo, num)
|
|
if key not in seen:
|
|
seen.add(key)
|
|
deps.append({"repo": repo, "number": num})
|
|
|
|
has_decl = not deps and has_declaration(body)
|
|
if has_decl:
|
|
warnings.append(
|
|
"Found a 'depends-on:' line but no valid dependency was parsed. "
|
|
"Declare dependencies on the same line as 'depends-on:', e.g. "
|
|
"depends-on: [{}/pull/<N> {}/pull/<M>]".format(*allowed_repos)
|
|
)
|
|
|
|
return {
|
|
"version": 1,
|
|
"pr_number": pr_number,
|
|
"head_sha": head_sha,
|
|
"status": "ok" if deps else "invalid" if has_decl else "none",
|
|
"dependencies": deps,
|
|
"warnings": warnings,
|
|
}
|
|
|
|
|
|
def dep_ref(dep):
|
|
return "{}/pull/{}".format(dep["repo"], dep["number"])
|
|
|
|
|
|
def _int_or_none(value):
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def main(argv):
|
|
body = os.environ.get("PR_BODY", "")
|
|
result = parse_dependencies(
|
|
body,
|
|
pr_number=_int_or_none(os.environ.get("PR_NUMBER")),
|
|
head_sha=os.environ.get("HEAD_SHA") or None,
|
|
)
|
|
refs = [dep_ref(d) for d in result["dependencies"]]
|
|
|
|
if "--print-state" in argv:
|
|
print(result["status"])
|
|
for r in refs:
|
|
print(r)
|
|
return 0
|
|
|
|
if "--github-output" in argv:
|
|
for w in result["warnings"]:
|
|
print("::warning::" + w)
|
|
out_lines = [
|
|
"depends_on=" + " ".join(refs),
|
|
"status=" + result["status"],
|
|
]
|
|
gh_out = os.environ.get("GITHUB_OUTPUT")
|
|
if gh_out:
|
|
with open(gh_out, "a", encoding="utf-8") as f:
|
|
f.write("\n".join(out_lines) + "\n")
|
|
else:
|
|
print("\n".join(out_lines))
|
|
# Write the report only when a depends-on declaration is present
|
|
# (status ok/invalid). status=none means there is nothing to report and
|
|
# nothing to comment; existing (historical) comments are left untouched.
|
|
report_path = os.environ.get("REPORT_PATH")
|
|
if report_path and result["status"] != "none":
|
|
parent = os.path.dirname(report_path)
|
|
if parent:
|
|
os.makedirs(parent, exist_ok=True)
|
|
with open(report_path, "w", encoding="utf-8") as f:
|
|
json.dump(result, f)
|
|
return 0
|
|
|
|
print(json.dumps(result))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv[1:]))
|