This is an automated email from the ASF dual-hosted git repository. acassis pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/nuttx.git
commit 4381d999f042ba22bf95a06126b06858e21a5c86 Author: Marco Casaroli <[email protected]> AuthorDate: Mon Oct 19 10:21:41 2026 +0200 .github/workflows: Skip depends-on PRs that are already merged. When a depends-on PR is merged, its commits are in master. Build and the memory report then cherry-pick the PR head again, the cherry-pick is empty or conflicts, and Fetch-Source fails. The author must remove the depends-on line and start CI again. Read the state of each dependency with the GitHub API. If the PR is merged and its merge commit is in the checkout, skip the PR. Apply open PRs, PRs merged into a different branch, and PRs whose merge commit is not in the checkout as before. If the API call fails, apply the dependency as before. A PR that is closed without merge is now an error: CI does not apply it, and the step fails with "closed without merge; remove it from depends-on or reopen it". Before, CI applied such a PR silently. The report marks a skipped dependency with "merged": true. A failure report marks a closed dependency with "closed": true and the error code dependency_closed. The comment shows "already merged, skipped", "closed without merge", and the reason for the failure. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marco Casaroli <[email protected]> --- .github/workflows/build.yml | 37 ++++++++++++++++++++++++++++++-- .github/workflows/depends-on-comment.yml | 14 ++++++------ .github/workflows/membrowse-report.yml | 22 +++++++++++++++++++ 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84f4a9ffc07..b7c97ffeecd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -146,6 +146,8 @@ jobs: - name: Apply depends-on PRs if: ${{ steps.gittargets.outputs.depends_on != '' }} shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -uo pipefail @@ -153,9 +155,10 @@ jobs: git config --global user.name "github-actions" # Pass only fixed error codes to the trusted comment workflow. + # $3 and $4 name a dependency that is closed without merge. mark_failed() { echo "::error::could not apply $1 ($2)" - python3 - "$2" <<'PY' + python3 - "$2" "${3:-}" "${4:-}" <<'PY' import json, sys p = "depends-on-report/result.json" try: @@ -164,6 +167,9 @@ jobs: d = {"version": 1, "pr_number": None, "head_sha": None, "dependencies": [], "warnings": []} d["status"] = "failed" d["error_code"] = sys.argv[1] + for dep in d.get("dependencies", []): + if [dep.get("repo"), str(dep.get("number"))] == sys.argv[2:4]: + dep["closed"] = True open(p, "w").write(json.dumps(d)) PY } @@ -205,8 +211,30 @@ jobs: fi DEP_SHA=$(git -C "$REPO_PATH" rev-parse "dep-${DEP_PR_NUM}") + + # Skip a dependency whose merge commit is already in the checkout. + # Its commits are in the target branch and cannot be applied again. + # A dependency that is closed without merge is an error. + if ! DEP_INFO=$(gh api "repos/${DEP_REPO}/pulls/${DEP_PR_NUM}" \ + --jq 'if .merged then "merged \(.merge_commit_sha)" else .state end'); then + echo "::warning::Could not read the state of ${DEP}; applying it." + DEP_INFO="" + fi + read -r DEP_STATE MERGE_SHA <<< "$DEP_INFO" + if [ "$DEP_STATE" = "merged" ] && \ + git -C "$REPO_PATH" merge-base --is-ancestor "$MERGE_SHA" HEAD 2>/dev/null; then + echo "Dependency ${DEP} is merged as ${MERGE_SHA}; skipping it" + printf '%s\t%s\t%s\tmerged\n' "$DEP_REPO" "$DEP_PR_NUM" "$DEP_SHA" >> depends-on-report/applied.tsv + continue + fi + printf '%s\t%s\t%s\n' "$DEP_REPO" "$DEP_PR_NUM" "$DEP_SHA" >> depends-on-report/applied.tsv + if [ "$DEP_STATE" = "closed" ]; then + echo "::error::Dependency ${DEP} is closed without merge; remove it from depends-on or reopen it." + mark_failed "$DEP" "dependency_closed" "$DEP_REPO" "$DEP_PR_NUM"; exit 1 + fi + # 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) @@ -237,12 +265,15 @@ jobs: python3 - <<'PY' import json shas = {} + merged = set() 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: + if len(p) in (3, 4): shas[(p[0], p[1])] = p[2] + if len(p) == 4 and p[3] == "merged": + merged.add((p[0], p[1])) except FileNotFoundError: pass with open("depends-on-report/result.json", encoding="utf-8") as f: @@ -251,6 +282,8 @@ jobs: key = (dep.get("repo"), str(dep.get("number"))) if key in shas: dep["head_sha"] = shas[key] + if key in merged: + dep["merged"] = True with open("depends-on-report/result.json", "w", encoding="utf-8") as f: json.dump(d, f) PY diff --git a/.github/workflows/depends-on-comment.yml b/.github/workflows/depends-on-comment.yml index b6ade4d341c..2e515bdfd18 100644 --- a/.github/workflows/depends-on-comment.yml +++ b/.github/workflows/depends-on-comment.yml @@ -135,6 +135,10 @@ jobs: 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)}` : ''; + const depLine = (d) => + `- https://github.com/${d.repo}/pull/${d.number}${shortSha(d.head_sha)}` + + (d.merged === true ? ' (already merged, skipped)' : '') + + (d.closed === true ? ' ❌ **closed without merge**' : ''); let body; if (status === 'ok') { @@ -142,9 +146,7 @@ jobs: 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'); + const list = deps.map(depLine).join('\n'); body = `${marker}\n` + `### 🔗 Cross-repo PR dependencies\n\n` + @@ -157,9 +159,7 @@ jobs: 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'); + const list = deps.map(depLine).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)', @@ -168,6 +168,8 @@ jobs: 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', + dependency_closed: 'a dependency PR is closed without merge; ' + + 'remove it from depends-on or reopen it', }; const code = typeof report.error_code === 'string' ? report.error_code : ''; const reason = REASONS[code] ? `\n\nReason: ${REASONS[code]}` : ''; diff --git a/.github/workflows/membrowse-report.yml b/.github/workflows/membrowse-report.yml index 07f35e2adf1..bafc4988e75 100644 --- a/.github/workflows/membrowse-report.yml +++ b/.github/workflows/membrowse-report.yml @@ -316,6 +316,28 @@ jobs: fi DEP_SHA=$(git -C "$REPO_PATH" rev-parse "dep-${DEP_NUM}") + + # Skip a merged dependency and reject a closed one, as build.yml + # does. + if ! DEP_INFO=$(gh api "repos/${DEP_REPO}/pulls/${DEP_NUM}" \ + --jq 'if .merged then "merged \(.merge_commit_sha)" else .state end'); then + echo "::warning::Could not read the state of ${DEP}; applying it." + DEP_INFO="" + fi + read -r DEP_STATE MERGE_SHA <<< "$DEP_INFO" + if [ "$DEP_STATE" = "merged" ] && \ + git -C "$REPO_PATH" merge-base --is-ancestor "$MERGE_SHA" HEAD 2>/dev/null; then + echo "Dependency ${DEP} is merged as ${MERGE_SHA}; skipping it." + echo "- \`${DEP}\` @ \`${DEP_SHA}\` (already merged, skipped)" \ + >> "${GITHUB_STEP_SUMMARY:-/dev/null}" + continue + fi + + if [ "$DEP_STATE" = "closed" ]; then + echo "::error::Dependency ${DEP} is closed without merge; remove it from depends-on or reopen it." + exit 1 + fi + if [ -z "$COMMITS" ]; then echo "Dependency ${DEP} is already included." echo "- \`${DEP}\` @ \`${DEP_SHA}\` (already in the checkout)" \
