mirror of
https://github.com/apache/nuttx.git
synced 2026-08-01 20:28:58 +00:00
ci: Support pull request dependencies via Depends-On.
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>
This commit is contained in:
parent
ddf350f1a3
commit
e73f7f7d0e
5 changed files with 987 additions and 1 deletions
197
.github/scripts/depends_on.py
vendored
Normal file
197
.github/scripts/depends_on.py
vendored
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
#!/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:]))
|
||||
293
.github/scripts/test_depends_on.py
vendored
Normal file
293
.github/scripts/test_depends_on.py
vendored
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
#!/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.
|
||||
#
|
||||
"""Unit tests for depends_on. Run: python3 -m unittest test_depends_on -v"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from unittest import mock
|
||||
|
||||
from depends_on import has_declaration, main, parse_dependencies
|
||||
|
||||
OS_REPO = "apache/nuttx"
|
||||
APPS_REPO = "apache/nuttx-apps"
|
||||
ALLOWED = (OS_REPO, APPS_REPO)
|
||||
|
||||
|
||||
def dep(repo, number):
|
||||
return {"repo": repo, "number": number}
|
||||
|
||||
|
||||
def parsed(body, allowed_repos):
|
||||
result = parse_dependencies(body, allowed_repos)
|
||||
return result["dependencies"], result["warnings"]
|
||||
|
||||
|
||||
class ParseBasicsTest(unittest.TestCase):
|
||||
def test_none(self):
|
||||
self.assertEqual(parsed("normal body\nno deps", ALLOWED), ([], []))
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(parsed("", ALLOWED), ([], []))
|
||||
self.assertEqual(parsed(None, ALLOWED), ([], []))
|
||||
|
||||
def test_single(self):
|
||||
d, w = parsed("depends-on: %s/pull/1234" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 1234)])
|
||||
self.assertEqual(w, [])
|
||||
|
||||
def test_full_url(self):
|
||||
d, _ = parsed("depends-on: https://github.com/%s/pull/1" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 1)])
|
||||
|
||||
def test_array_mixed(self):
|
||||
body = "depends-on: [%s/pull/1 https://github.com/%s/pull/2]" % (
|
||||
APPS_REPO,
|
||||
OS_REPO,
|
||||
)
|
||||
d, _ = parsed(body, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 1), dep(OS_REPO, 2)])
|
||||
|
||||
def test_dedup(self):
|
||||
body = "depends-on: [%s/pull/1 %s/pull/1]" % (APPS_REPO, APPS_REPO)
|
||||
d, _ = parsed(body, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 1)])
|
||||
|
||||
def test_case_insensitive(self):
|
||||
d, _ = parsed("Depends-On: %s/pull/7" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 7)])
|
||||
|
||||
def test_non_allowlisted(self):
|
||||
d, w = parsed("depends-on: someone/other/pull/1", ALLOWED)
|
||||
self.assertEqual(d, [])
|
||||
self.assertTrue(any("unsupported" in x.lower() for x in w))
|
||||
|
||||
def test_typo_push(self):
|
||||
d, w = parsed("depends-on: %s/push/1" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d, [])
|
||||
self.assertTrue(w)
|
||||
|
||||
def test_non_numeric(self):
|
||||
d, w = parsed("depends-on: %s/pull/abc" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d, [])
|
||||
self.assertTrue(w)
|
||||
|
||||
|
||||
class NumberBoundsTest(unittest.TestCase):
|
||||
def test_zero_rejected(self):
|
||||
self.assertEqual(parsed("depends-on: %s/pull/0" % APPS_REPO, ALLOWED)[0], [])
|
||||
|
||||
def test_leading_zero_rejected(self):
|
||||
self.assertEqual(parsed("depends-on: %s/pull/007" % APPS_REPO, ALLOWED)[0], [])
|
||||
|
||||
def test_16_digit_within_safe_ok(self):
|
||||
# 16-digit but <= MAX_SAFE_INTEGER is valid (no business cap).
|
||||
d, _ = parsed("depends-on: %s/pull/1234567890123456" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 1234567890123456)])
|
||||
|
||||
def test_max_safe_integer_ok(self):
|
||||
d, _ = parsed("depends-on: %s/pull/9007199254740991" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 9007199254740991)])
|
||||
|
||||
def test_above_safe_integer_rejected(self):
|
||||
# MAX_SAFE_INTEGER + 1
|
||||
self.assertEqual(
|
||||
parsed("depends-on: %s/pull/9007199254740992" % APPS_REPO, ALLOWED)[0],
|
||||
[],
|
||||
)
|
||||
|
||||
def test_twenty_digits_rejected(self):
|
||||
self.assertEqual(
|
||||
parsed("depends-on: %s/pull/99999999999999999999" % APPS_REPO, ALLOWED)[0],
|
||||
[],
|
||||
)
|
||||
|
||||
def test_huge_number_does_not_crash(self):
|
||||
# thousands of digits must be rejected without raising (no int() blowup)
|
||||
body = "depends-on: %s/pull/%s" % (APPS_REPO, "9" * 5000)
|
||||
d, _ = parsed(body, ALLOWED)
|
||||
self.assertEqual(d, [])
|
||||
|
||||
|
||||
class UnicodeLineTest(unittest.TestCase):
|
||||
def test_u2028_not_a_new_line(self):
|
||||
# U+2028 must not be treated as a line break that starts a new marker.
|
||||
body = "intro text\u2028depends-on: %s/pull/1" % APPS_REPO
|
||||
self.assertFalse(has_declaration(body))
|
||||
self.assertEqual(parsed(body, ALLOWED)[0], [])
|
||||
|
||||
def test_crlf_and_cr_are_line_breaks(self):
|
||||
d1, _ = parsed("x\r\ndepends-on: %s/pull/1" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d1, [dep(APPS_REPO, 1)])
|
||||
d2, _ = parsed("x\rdepends-on: %s/pull/2" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d2, [dep(APPS_REPO, 2)])
|
||||
|
||||
def test_u2028_within_declaration_line_not_split(self):
|
||||
# On a real depends-on line, a U+2028-joined pair is not split into two
|
||||
# tokens (ASCII-only separators); it becomes one invalid token.
|
||||
body = "depends-on: %s/pull/1\u2028%s/pull/2" % (APPS_REPO, APPS_REPO)
|
||||
self.assertEqual(parsed(body, ALLOWED)[0], [])
|
||||
|
||||
|
||||
class LineAnchorTest(unittest.TestCase):
|
||||
def test_midline_prose_ignored(self):
|
||||
body = "Please see depends-on: %s/pull/13 for the example." % APPS_REPO
|
||||
self.assertFalse(has_declaration(body))
|
||||
self.assertEqual(parsed(body, ALLOWED), ([], []))
|
||||
|
||||
def test_not_depends_on_ignored(self):
|
||||
body = "not-depends-on: %s/pull/1" % APPS_REPO
|
||||
self.assertFalse(has_declaration(body))
|
||||
self.assertEqual(parsed(body, ALLOWED), ([], []))
|
||||
|
||||
def test_code_fence_ignored_backtick(self):
|
||||
body = "```\ndepends-on: %s/pull/1\n```" % APPS_REPO
|
||||
self.assertFalse(has_declaration(body))
|
||||
self.assertEqual(parsed(body, ALLOWED), ([], []))
|
||||
|
||||
def test_code_fence_ignored_tilde(self):
|
||||
body = "~~~\ndepends-on: %s/pull/1\n~~~" % APPS_REPO
|
||||
self.assertEqual(parsed(body, ALLOWED), ([], []))
|
||||
|
||||
def test_leading_whitespace_ok(self):
|
||||
d, _ = parsed(" depends-on: %s/pull/5" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 5)])
|
||||
|
||||
def test_four_space_indent_is_code_ignored(self):
|
||||
# 4+ leading spaces = CommonMark indented code block -> ignored.
|
||||
body = " depends-on: %s/pull/1" % APPS_REPO
|
||||
self.assertFalse(has_declaration(body))
|
||||
self.assertEqual(parsed(body, ALLOWED), ([], []))
|
||||
|
||||
def test_tab_indent_is_code_ignored(self):
|
||||
body = "\tdepends-on: %s/pull/1" % APPS_REPO
|
||||
self.assertFalse(has_declaration(body))
|
||||
self.assertEqual(parsed(body, ALLOWED), ([], []))
|
||||
|
||||
def test_indented_fence_is_not_a_fence(self):
|
||||
# A ``` indented by 4+ spaces is not a fence opener (CommonMark), so the
|
||||
# following column-0 depends-on: must still be parsed.
|
||||
body = " ```\ndepends-on: %s/pull/1\n ```" % APPS_REPO
|
||||
d, _ = parsed(body, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 1)])
|
||||
|
||||
def test_real_after_prose_still_parsed(self):
|
||||
body = "See docs.\ndepends-on: %s/pull/9\nthanks" % APPS_REPO
|
||||
d, _ = parsed(body, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 9)])
|
||||
|
||||
|
||||
class HostBoundaryTest(unittest.TestCase):
|
||||
def test_gitlab_rejected(self):
|
||||
d, w = parsed("depends-on: gitlab.com/%s/pull/7" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d, [])
|
||||
self.assertTrue(w) # declaration present but nothing valid
|
||||
|
||||
def test_bare_github_com_without_https_rejected(self):
|
||||
d, _ = parsed("depends-on: github.com/%s/pull/7" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d, [])
|
||||
|
||||
def test_http_github_rejected(self):
|
||||
# only https://github.com/ is accepted as URL form
|
||||
d, _ = parsed("depends-on: http://github.com/%s/pull/7" % APPS_REPO, ALLOWED)
|
||||
self.assertEqual(d, [])
|
||||
|
||||
|
||||
class MultiDependencyTest(unittest.TestCase):
|
||||
def test_multiple_depends_on_lines(self):
|
||||
# Multiple dependencies via several depends-on: lines (no continuation).
|
||||
body = (
|
||||
"depends-on: %s/pull/1\n"
|
||||
"depends-on: https://github.com/%s/pull/2\n" % (APPS_REPO, OS_REPO)
|
||||
)
|
||||
d, w = parsed(body, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 1), dep(OS_REPO, 2)])
|
||||
self.assertEqual(w, [])
|
||||
|
||||
def test_inline_array(self):
|
||||
body = "depends-on: [%s/pull/1 %s/pull/2]" % (APPS_REPO, OS_REPO)
|
||||
d, _ = parsed(body, ALLOWED)
|
||||
self.assertEqual(d, [dep(APPS_REPO, 1), dep(OS_REPO, 2)])
|
||||
|
||||
def test_bullet_list_not_parsed(self):
|
||||
# A bullet list under a bare marker is not a supported form: the marker
|
||||
# line has no ref and the '- ...' lines are not declarations.
|
||||
body = "depends-on:\n" "- %s/pull/1\n" "- %s/pull/2\n" % (APPS_REPO, APPS_REPO)
|
||||
d, w = parsed(body, ALLOWED)
|
||||
self.assertEqual(d, []) # nothing parsed
|
||||
self.assertTrue(w) # declaration present but invalid
|
||||
|
||||
|
||||
class ParseResultStatusTest(unittest.TestCase):
|
||||
def test_none(self):
|
||||
self.assertEqual(
|
||||
parse_dependencies("hi", allowed_repos=ALLOWED)["status"], "none"
|
||||
)
|
||||
|
||||
def test_ok(self):
|
||||
r = parse_dependencies(
|
||||
"depends-on: %s/pull/9" % APPS_REPO,
|
||||
pr_number=42,
|
||||
head_sha="abc",
|
||||
allowed_repos=ALLOWED,
|
||||
)
|
||||
self.assertEqual(r["status"], "ok")
|
||||
self.assertEqual(r["dependencies"], [dep(APPS_REPO, 9)])
|
||||
self.assertEqual((r["pr_number"], r["head_sha"], r["version"]), (42, "abc", 1))
|
||||
|
||||
def test_invalid(self):
|
||||
self.assertEqual(
|
||||
parse_dependencies(
|
||||
"depends-on: %s/push/1" % APPS_REPO, allowed_repos=ALLOWED
|
||||
)["status"],
|
||||
"invalid",
|
||||
)
|
||||
|
||||
def test_code_fence_is_none(self):
|
||||
self.assertEqual(
|
||||
parse_dependencies(
|
||||
"```\ndepends-on: %s/pull/1\n```" % APPS_REPO, allowed_repos=ALLOWED
|
||||
)["status"],
|
||||
"none",
|
||||
)
|
||||
|
||||
|
||||
class PrintStateTest(unittest.TestCase):
|
||||
def test_dependency_order_is_part_of_state(self):
|
||||
bodies = (
|
||||
"depends-on: [apache/nuttx/pull/1 apache/nuttx-apps/pull/2]",
|
||||
"depends-on: [apache/nuttx-apps/pull/2 apache/nuttx/pull/1]",
|
||||
)
|
||||
outputs = []
|
||||
for body in bodies:
|
||||
output = io.StringIO()
|
||||
with mock.patch.dict(os.environ, {"PR_BODY": body}, clear=True):
|
||||
with redirect_stdout(output):
|
||||
self.assertEqual(main(["--print-state"]), 0)
|
||||
outputs.append(output.getvalue().splitlines())
|
||||
|
||||
self.assertEqual(
|
||||
outputs[0],
|
||||
["ok", "apache/nuttx/pull/1", "apache/nuttx-apps/pull/2"],
|
||||
)
|
||||
self.assertEqual(
|
||||
outputs[1],
|
||||
["ok", "apache/nuttx-apps/pull/2", "apache/nuttx/pull/1"],
|
||||
)
|
||||
self.assertNotEqual(outputs[0], outputs[1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
216
.github/workflows/build.yml
vendored
216
.github/workflows/build.yml
vendored
|
|
@ -14,6 +14,7 @@ name: Build
|
|||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
paths-ignore:
|
||||
- "AUTHORS"
|
||||
- "CONTRIBUTING.md"
|
||||
|
|
@ -35,16 +36,104 @@ permissions:
|
|||
|
||||
concurrency:
|
||||
group: build-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
# Edited runs do not request cancellation of an active code build.
|
||||
# GitHub may still replace an older pending run in this concurrency group.
|
||||
cancel-in-progress: ${{ github.event.action != 'edited' }}
|
||||
|
||||
jobs:
|
||||
# Gate heavy CI on dependency-changing edits.
|
||||
Changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_build: ${{ steps.gate.outputs.should_build }}
|
||||
steps:
|
||||
# Do not let PR code control its own edit gate.
|
||||
- name: Checkout base-branch CI scripts
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.action == 'edited' }}
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
fetch-depth: 1
|
||||
path: base-ci
|
||||
continue-on-error: true
|
||||
- name: Checkout PR CI scripts (fallback)
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.action == 'edited' }}
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
fetch-depth: 1
|
||||
path: pr-ci
|
||||
- name: Decide whether to run CI
|
||||
id: gate
|
||||
shell: bash
|
||||
env:
|
||||
ACTION: ${{ github.event.action }}
|
||||
NEW_BODY: ${{ github.event.pull_request.body }}
|
||||
OLD_BODY: ${{ github.event.changes.body.from }}
|
||||
BODY_CHANGE: ${{ toJSON(github.event.changes.body) }}
|
||||
BASE_CHANGE: ${{ toJSON(github.event.changes.base) }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${ACTION:-}" != "edited" ]; then
|
||||
echo "Event '${ACTION:-push}': running CI."
|
||||
echo "should_build=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$BASE_CHANGE" != "null" ]; then
|
||||
echo "::notice::PR base branch changed; running CI."
|
||||
echo "should_build=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$BODY_CHANGE" = "null" ]; then
|
||||
echo "::notice::PR edited but body unchanged; no code/dependency change, skipping CI."
|
||||
echo "should_build=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PARSER="pr-ci/.github/scripts/depends_on.py"
|
||||
if [ -f "base-ci/.github/scripts/depends_on.py" ]; then
|
||||
PARSER="base-ci/.github/scripts/depends_on.py"
|
||||
echo "Using base-branch parser for the gate."
|
||||
else
|
||||
echo "::notice::Base branch has no depends_on.py yet; using PR parser for the gate (bootstrap)."
|
||||
fi
|
||||
|
||||
# Include status so invalid declarations also retrigger reporting.
|
||||
NEW_STATE="$(PR_BODY="$NEW_BODY" python3 "$PARSER" --print-state)"
|
||||
OLD_STATE="$(PR_BODY="$OLD_BODY" python3 "$PARSER" --print-state)"
|
||||
if [ "$NEW_STATE" != "$OLD_STATE" ]; then
|
||||
echo "depends-on state changed; running CI."
|
||||
echo "should_build=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::notice::No depends-on change on this edit; no code change, skipping CI."
|
||||
echo "should_build=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Fetch the source from nuttx and nuttx-apps repos
|
||||
Fetch-Source:
|
||||
needs: Changes
|
||||
if: ${{ needs.Changes.outputs.should_build == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout CI scripts
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
fetch-depth: 1
|
||||
- name: Determine Target Branches
|
||||
id: gittargets
|
||||
shell: bash
|
||||
env:
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
REPORT_PATH: depends-on-report/result.json
|
||||
run: |
|
||||
OS_REF=""
|
||||
APPS_REF=""
|
||||
|
|
@ -91,6 +180,11 @@ jobs:
|
|||
esac
|
||||
fi
|
||||
|
||||
# Release and backport PRs ignore dependencies.
|
||||
if [ -n "$PR_BODY" ] && [ "$GITHUB_BASE_REF" = "master" ]; then
|
||||
python3 .github/scripts/depends_on.py --github-output
|
||||
fi
|
||||
|
||||
echo "os_ref=$OS_REF" >> $GITHUB_OUTPUT
|
||||
echo "apps_ref=$APPS_REF" >> $GITHUB_OUTPUT
|
||||
|
||||
|
|
@ -112,6 +206,126 @@ jobs:
|
|||
path: sources/apps
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Apply depends-on PRs
|
||||
if: ${{ steps.gittargets.outputs.depends_on != '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -uo pipefail
|
||||
|
||||
git config --global user.email "actions@github.com"
|
||||
git config --global user.name "github-actions"
|
||||
|
||||
# Pass only fixed error codes to the trusted comment workflow.
|
||||
mark_failed() {
|
||||
echo "::error::could not apply $1 ($2)"
|
||||
python3 - "$2" <<'PY'
|
||||
import json, sys
|
||||
p = "depends-on-report/result.json"
|
||||
try:
|
||||
d = json.load(open(p))
|
||||
except Exception:
|
||||
d = {"version": 1, "pr_number": None, "head_sha": None, "dependencies": [], "warnings": []}
|
||||
d["status"] = "failed"
|
||||
d["error_code"] = sys.argv[1]
|
||||
open(p, "w").write(json.dumps(d))
|
||||
PY
|
||||
}
|
||||
|
||||
# Parse before the loop so process substitution cannot hide errors.
|
||||
if ! python3 - > depends-on-report/deps.tsv <<'PY'
|
||||
import json
|
||||
with open("depends-on-report/result.json", encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
for x in d["dependencies"]:
|
||||
print("%s\t%d" % (x["repo"], x["number"]))
|
||||
PY
|
||||
then
|
||||
echo "::error::could not read the dependency report"
|
||||
mark_failed "depends-on" "report_parse_failed"; exit 1
|
||||
fi
|
||||
|
||||
: > depends-on-report/applied.tsv
|
||||
|
||||
while IFS=$'\t' read -r DEP_REPO DEP_PR_NUM; do
|
||||
[ -n "$DEP_REPO" ] || continue
|
||||
DEP="${DEP_REPO}/pull/${DEP_PR_NUM}"
|
||||
|
||||
case "$DEP_REPO" in
|
||||
"apache/nuttx") REPO_PATH="sources/nuttx" ;;
|
||||
"apache/nuttx-apps") REPO_PATH="sources/apps" ;;
|
||||
*)
|
||||
echo "::error::Unsupported dependency repo: $DEP_REPO"
|
||||
mark_failed "$DEP" "unsupported_repo"; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "Applying dependency ${DEP}"
|
||||
if [ -f "$REPO_PATH/.git/shallow" ]; then
|
||||
git -C "$REPO_PATH" fetch --unshallow origin || true
|
||||
fi
|
||||
if ! git -C "$REPO_PATH" fetch origin "pull/${DEP_PR_NUM}/head:dep-${DEP_PR_NUM}"; then
|
||||
echo "::error::Could not fetch ${DEP} (the PR may not exist)."
|
||||
mark_failed "$DEP" "fetch_failed"; exit 1
|
||||
fi
|
||||
|
||||
DEP_SHA=$(git -C "$REPO_PATH" rev-parse "dep-${DEP_PR_NUM}")
|
||||
printf '%s\t%s\t%s\n' "$DEP_REPO" "$DEP_PR_NUM" "$DEP_SHA" >> depends-on-report/applied.tsv
|
||||
|
||||
# Stop on unrelated histories; HEAD..dep would otherwise include
|
||||
# every dependency commit and could cherry-pick unrelated changes.
|
||||
COMMON_BASE=$(git -C "$REPO_PATH" merge-base "dep-${DEP_PR_NUM}" HEAD || true)
|
||||
if [ -z "$COMMON_BASE" ]; then
|
||||
echo "::error::Could not find common base for ${DEP}"
|
||||
mark_failed "$DEP" "no_common_base"; exit 1
|
||||
fi
|
||||
|
||||
COMMITS=$(git -C "$REPO_PATH" rev-list --reverse "HEAD..dep-${DEP_PR_NUM}") || {
|
||||
echo "::error::Could not list commits for ${DEP}"
|
||||
mark_failed "$DEP" "rev_list_failed"; exit 1
|
||||
}
|
||||
|
||||
if [ -z "$COMMITS" ]; then
|
||||
echo "Dependency ${DEP} is already included"
|
||||
continue
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
if ! git -C "$REPO_PATH" cherry-pick $COMMITS; then
|
||||
echo "::error::cherry-pick failed for ${DEP}."
|
||||
echo "::error::If your PR contains merge commits, please rebase instead of merge."
|
||||
git -C "$REPO_PATH" cherry-pick --abort || true
|
||||
mark_failed "$DEP" "cherry_pick_conflict"; exit 1
|
||||
fi
|
||||
done < depends-on-report/deps.tsv
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
shas = {}
|
||||
try:
|
||||
with open("depends-on-report/applied.tsv", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
p = line.rstrip("\n").split("\t")
|
||||
if len(p) == 3:
|
||||
shas[(p[0], p[1])] = p[2]
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
with open("depends-on-report/result.json", encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
for dep in d.get("dependencies", []):
|
||||
key = (dep.get("repo"), str(dep.get("number")))
|
||||
if key in shas:
|
||||
dep["head_sha"] = shas[key]
|
||||
with open("depends-on-report/result.json", "w", encoding="utf-8") as f:
|
||||
json.dump(d, f)
|
||||
PY
|
||||
|
||||
# Apply failures rewrite the report file; the step output remains "ok".
|
||||
- name: Upload depends-on report
|
||||
if: ${{ always() && (steps.gittargets.outputs.status == 'ok' || steps.gittargets.outputs.status == 'invalid') }}
|
||||
uses: actions/upload-artifact@v7.0.1
|
||||
with:
|
||||
name: depends-on-report
|
||||
path: depends-on-report/
|
||||
|
||||
- name: Tar sources
|
||||
run: tar zcf sources.tar.gz sources
|
||||
|
||||
|
|
|
|||
208
.github/workflows/depends-on-comment.yml
vendored
Normal file
208
.github/workflows/depends-on-comment.yml
vendored
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# 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.
|
||||
#
|
||||
# Posts validated dependency reports without checking out PR code.
|
||||
# workflow_run isolates write permission and uses the default-branch workflow.
|
||||
|
||||
name: Depends-On Comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
actions: read # download an artifact from the triggering run
|
||||
pull-requests: write
|
||||
|
||||
# Keep this artifact allow-list in sync with build.yml.
|
||||
env:
|
||||
NUTTX_REPO: apache/nuttx
|
||||
APPS_REPO: apache/nuttx-apps
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download depends-on report
|
||||
id: dl
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: depends-on-report
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path: report
|
||||
continue-on-error: true
|
||||
|
||||
- name: Comment on the PR
|
||||
if: ${{ steps.dl.outcome == 'success' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const allow = [process.env.NUTTX_REPO, process.env.APPS_REPO].filter(Boolean);
|
||||
|
||||
// Validate the untrusted Build artifact's structure and safe
|
||||
// rendering fields without executing fork code.
|
||||
let report;
|
||||
try {
|
||||
report = JSON.parse(fs.readFileSync('report/result.json', 'utf8'));
|
||||
} catch (e) {
|
||||
core.info('No valid depends-on report; nothing to comment.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (report.version !== 1) {
|
||||
core.info('Unexpected report version; skipping.');
|
||||
return;
|
||||
}
|
||||
const status = report.status;
|
||||
if (status !== 'ok' && status !== 'invalid' && status !== 'failed') {
|
||||
core.info(`Report status='${status}'; nothing to comment.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Match the Python parser's numeric and repository limits. Reject
|
||||
// the whole report instead of silently dropping unsafe entries.
|
||||
const isSafeNumber = (n) => Number.isSafeInteger(n) && n > 0;
|
||||
const rawDeps = Array.isArray(report.dependencies) ? report.dependencies : null;
|
||||
if (rawDeps === null) {
|
||||
core.info('Report dependencies are not an array; skipping.');
|
||||
return;
|
||||
}
|
||||
const deps = rawDeps.filter((d) => d && typeof d.repo === 'string'
|
||||
&& allow.includes(d.repo) && isSafeNumber(d.number));
|
||||
if (deps.length !== rawDeps.length) {
|
||||
core.info('Report contains an invalid dependency entry; skipping.');
|
||||
return;
|
||||
}
|
||||
const keys = deps.map((d) => `${d.repo}#${d.number}`);
|
||||
if (new Set(keys).size !== keys.length) {
|
||||
core.info('Report contains duplicate dependencies; skipping.');
|
||||
return;
|
||||
}
|
||||
if (status === 'invalid' && deps.length !== 0) {
|
||||
core.info('status=invalid but dependencies are present; skipping.');
|
||||
return;
|
||||
}
|
||||
const isFullSha = (s) => typeof s === 'string' && /^[0-9a-f]{40}$/i.test(s);
|
||||
if (status === 'ok' && !deps.every((d) => isFullSha(d.head_sha))) {
|
||||
core.info('status=ok but a full dependency head SHA is missing; skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Bind the report to both the triggering run and the PR's current
|
||||
// head. Stale, cancelled, or mismatched reports must not comment.
|
||||
const headSha = context.payload.workflow_run.head_sha;
|
||||
if (report.head_sha !== headSha) {
|
||||
core.info('Report head_sha does not match workflow_run head_sha; skipping.');
|
||||
return;
|
||||
}
|
||||
const claimed = Number(report.pr_number);
|
||||
|
||||
let prNum = null;
|
||||
if (Number.isSafeInteger(claimed) && claimed > 0) {
|
||||
try {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner, repo, pull_number: claimed });
|
||||
if (pr.head.sha === headSha) prNum = claimed;
|
||||
} catch (e) {
|
||||
prNum = null;
|
||||
}
|
||||
}
|
||||
if (prNum === null) {
|
||||
core.info(
|
||||
"workflow_run head_sha does not match the claimed PR's current " +
|
||||
'head (stale/cancelled run, or artifact mismatch); skipping comment.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Preserve one idempotent comment per Build run.
|
||||
const runId = context.payload.workflow_run.id;
|
||||
const marker = `<!-- nuttx-depends-on-bot run-${runId} -->`;
|
||||
const runUrl = context.payload.workflow_run.html_url;
|
||||
const shortSha = (s) =>
|
||||
(typeof s === 'string' && /^[0-9a-f]{7,40}$/i.test(s)) ? ` @ ${s.slice(0, 10)}` : '';
|
||||
|
||||
let body;
|
||||
if (status === 'ok') {
|
||||
if (deps.length === 0) {
|
||||
core.info('status=ok but no valid dependencies after validation; skipping.');
|
||||
return;
|
||||
}
|
||||
const list = deps
|
||||
.map((d) => `- https://github.com/${d.repo}/pull/${d.number}${shortSha(d.head_sha)}`)
|
||||
.join('\n');
|
||||
body =
|
||||
`${marker}\n` +
|
||||
`### 🔗 Cross-repo PR dependencies\n\n` +
|
||||
`The read-only Build run reported the following dependent ` +
|
||||
`PR(s) and fetched head SHA(s):\n\n` +
|
||||
`${list}\n\n` +
|
||||
`CI run: ${runUrl}`;
|
||||
} else if (status === 'failed') {
|
||||
if (deps.length === 0) {
|
||||
core.info('status=failed but no valid dependencies after validation; skipping.');
|
||||
return;
|
||||
}
|
||||
const list = deps
|
||||
.map((d) => `- https://github.com/${d.repo}/pull/${d.number}${shortSha(d.head_sha)}`)
|
||||
.join('\n');
|
||||
// Render only fixed text selected by an allowed error code.
|
||||
const REASONS = {
|
||||
fetch_failed: 'the dependency PR could not be fetched (it may not exist)',
|
||||
no_common_base: 'no common base with the dependency PR',
|
||||
rev_list_failed: 'could not determine the dependency commits',
|
||||
cherry_pick_conflict: 'cherry-pick failed (if your PR has merge commits, rebase instead)',
|
||||
unsupported_repo: 'the dependency repository is not supported',
|
||||
report_parse_failed: 'the dependency report could not be read',
|
||||
};
|
||||
const code = typeof report.error_code === 'string' ? report.error_code : '';
|
||||
const reason = REASONS[code] ? `\n\nReason: ${REASONS[code]}` : '';
|
||||
body =
|
||||
`${marker}\n` +
|
||||
`### ❌ Cross-repo dependency could not be applied\n\n` +
|
||||
`The Build report says the declared dependency PR(s) could not ` +
|
||||
`be applied, so CI did **not** run against the combined code:\n\n` +
|
||||
`${list}${reason}\n\n` +
|
||||
`CI run: ${runUrl}`;
|
||||
} else {
|
||||
const example = `depends-on: [${allow[0] || 'owner/repo'}/pull/<N> ` +
|
||||
`${allow[1] || 'owner/repo'}/pull/<M>]`;
|
||||
body =
|
||||
`${marker}\n` +
|
||||
`### ⚠️ \`depends-on\` could not be parsed\n\n` +
|
||||
`A \`depends-on:\` line was found in the PR description, but no ` +
|
||||
`valid dependency was parsed. Supported repositories: ` +
|
||||
`\`${allow.join('`, `')}\`; the PR id must be numeric.\n\n` +
|
||||
`Expected format:\n\n\`\`\`\n${example}\n\`\`\`\n\n` +
|
||||
`CI run: ${runUrl}`;
|
||||
}
|
||||
|
||||
// Update only this run's bot comment; otherwise create one.
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number: prNum, per_page: 100 });
|
||||
const existing = comments.find((c) =>
|
||||
c.user && c.user.type === 'Bot' && c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo, comment_id: existing.id, body });
|
||||
core.info(`Updated this run's comment on PR #${prNum}.`);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: prNum, body });
|
||||
core.info(`Created a new comment on PR #${prNum}.`);
|
||||
}
|
||||
|
|
@ -108,6 +108,80 @@ The name of this workflow is deceiving, as it does not only build
|
|||
configurations, but also normalizes them and runs :doc:`NTFC </testing/ntfc>`
|
||||
tests on architectures that support it.
|
||||
|
||||
Pull Request Dependencies
|
||||
-------------------------
|
||||
|
||||
NuttX and ``nuttx-apps`` are built together. A change that spans both
|
||||
repositories may therefore need to test two pull requests together before
|
||||
either one is merged. A pull request targeting ``master`` can declare same- or
|
||||
cross-repository dependencies in its description.
|
||||
|
||||
The recommended form is one dependency per line:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Depends-On: https://github.com/apache/nuttx-apps/pull/1234
|
||||
Depends-On: https://github.com/apache/nuttx/pull/5678
|
||||
|
||||
Alternatively, multiple dependencies can use a single bracket list:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Depends-On: [apache/nuttx-apps/pull/1234 https://github.com/apache/nuttx/pull/5678]
|
||||
|
||||
The ``Depends-On:`` marker is case-insensitive; ``Depends-On:``,
|
||||
``depends-on:``, and mixed-case spellings are equivalent. This documentation
|
||||
uses ``Depends-On:`` as the canonical form.
|
||||
|
||||
References may use either a complete ``https://github.com/...`` URL or the
|
||||
short ``owner/repository/pull/number`` form, and the forms may be mixed. Each
|
||||
accepted reference must identify a pull request in ``apache/nuttx`` or
|
||||
``apache/nuttx-apps``. Other tokens are ignored; a declaration is invalid only
|
||||
when a ``Depends-On:`` marker is present but no valid dependency can be parsed.
|
||||
Multiple declarations are applied in their listed order, and duplicate
|
||||
references are applied only once. Each declaration must remain on one line;
|
||||
Markdown bullet-list continuations are not supported. Pull requests without a
|
||||
``Depends-On:`` declaration retain the normal CI source selection.
|
||||
Declarations in release and backport pull requests are ignored because
|
||||
dependencies are applied only when the target branch is ``master``.
|
||||
|
||||
The ``Fetch-Source`` job fetches each valid dependency's exact head SHA and
|
||||
cherry-picks its commits into the corresponding checkout before the existing
|
||||
build matrix runs. Invalid declarations are not applied: CI continues with the
|
||||
normal source selection and posts a warning that the declaration could not be
|
||||
parsed. If a valid dependency cannot be fetched, has no common history, its
|
||||
commit list cannot be determined, or it causes a cherry-pick conflict,
|
||||
``Fetch-Source`` fails instead of silently testing without the requested
|
||||
dependency.
|
||||
|
||||
When a valid dependency report is available, the follow-up comment reports one
|
||||
of three outcomes:
|
||||
|
||||
* successfully applied dependencies and their fetched head SHAs (abbreviated in
|
||||
the comment)
|
||||
* a declaration from which no valid dependency could be parsed
|
||||
* valid dependencies that could not be applied and the failure reason
|
||||
|
||||
The write-capable follow-up workflow does not execute fork code. It validates
|
||||
the untrusted report's structure, repository allow-list, run and current PR
|
||||
head binding, and fixed rendering fields before commenting. These checks make
|
||||
posting the report safe, but do not independently attest that the dependency
|
||||
was applied; the comment reflects the result produced by the read-only Build
|
||||
workflow.
|
||||
|
||||
Editing the pull request description triggers the CI dependency gate. The
|
||||
resource-intensive build jobs run again when the base branch or ordered parsed
|
||||
dependency state changes, so reordering dependencies also triggers a build.
|
||||
Unrelated description edits run only the gate and do not request cancellation
|
||||
of an already-running Build. GitHub may still replace an older pending run in
|
||||
the same concurrency group. Updating a dependency pull request does not
|
||||
automatically trigger the initiating pull request, so its CI must be rerun to
|
||||
test the new dependency head.
|
||||
|
||||
The combined result belongs to the initiating pull request. It does not set a
|
||||
status on dependency pull requests, merge them automatically, or replace the
|
||||
need to coordinate their merge order.
|
||||
|
||||
The steps executed in the build workflow are:
|
||||
|
||||
1. Fetch source: checks out ``nuttx`` and ``nuttx-apps`` repos. The source files
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue