diff --git a/.github/workflows/testpr.yaml b/.github/workflows/testpr.yaml index e71f74ab..dd008b76 100644 --- a/.github/workflows/testpr.yaml +++ b/.github/workflows/testpr.yaml @@ -77,6 +77,9 @@ jobs: run: pixi run test build: + permissions: + actions: write # drop this pull request's superseded build caches + contents: read needs: plan if: needs.plan.outputs.distros != '[]' strategy: @@ -242,12 +245,31 @@ jobs: if [ -n "$newest" ]; then echo "Removing $newest"; rm -f "$newest"; fi - name: Save build cache + id: save-cache if: ${{ always() && contains(fromJSON('["success","failure","cancelled"]'), steps.build-recipes.outcome) }} uses: actions/cache/save@v5 with: path: ${{ env.CACHE_DIR }} key: ${{ runner.os }}-${{ matrix.distro }}-${{ matrix.target.platform }}-pr-${{ github.event.pull_request.number }}-${{ github.run_id }}-${{ github.run_attempt }} + # Only the newest cache of a pull request/distribution/platform is restored; the ones + # of earlier runs and attempts only push others out of the repository's 10 GB, so + # full rebuilds lost their progress. (Read-only token on forks: then this is skipped.) + - name: Drop this pull request's superseded build caches + if: ${{ always() && steps.save-cache.outcome == 'success' && github.event.pull_request.number }} + continue-on-error: true + shell: bash + env: + GH_TOKEN: ${{ github.token }} + PREFIX: ${{ runner.os }}-${{ matrix.distro }}-${{ matrix.target.platform }}-pr-${{ github.event.pull_request.number }}- + KEEP: ${{ runner.os }}-${{ matrix.distro }}-${{ matrix.target.platform }}-pr-${{ github.event.pull_request.number }}-${{ github.run_id }}-${{ github.run_attempt }} + run: | + gh cache list --repo "$GITHUB_REPOSITORY" --key "$PREFIX" --limit 100 --json id,key \ + --jq ".[] | select(.key != \"$KEEP\") | .id" | + while read -r id; do + gh cache delete "$id" --repo "$GITHUB_REPOSITORY" && echo "deleted cache $id" + done + - name: Generate the staged build workflows (catches post-merge problems) shell: bash -l {0} run: | @@ -290,12 +312,16 @@ jobs: echo "$marker" echo "### ABI check (linux-64)" echo + cat "${files[@]}" + echo "
About this check" + echo echo "Each package this pull request built, compared with its newest released build (\`tools/abi_check.py\`, libabigail):" echo "- **compatible** / **additions**: the package can be bumped on its own;" echo "- **soname** / **incompatible**: the packages that depend on it need a rebuild (build number bump) in the same pull request." echo - cat "${files[@]}" echo "Exported symbols are compared (the released packages are stripped, so struct layouts aren't); unchanged installed headers rule out layout and inline changes, changed ones are listed for review. Dependents to rebuild are the released ones linking to a changed library. A changed pin is part of the comparison." + echo + echo "
" } > body.md if [ -n "$existing" ]; then gh api -X PATCH "repos/$REPO/issues/comments/$existing" -F body=@body.md > /dev/null diff --git a/tools/abi_check.py b/tools/abi_check.py index f2860d93..3fcf15c3 100644 --- a/tools/abi_check.py +++ b/tools/abi_check.py @@ -505,9 +505,10 @@ def rebuild_snippet(results: dict, distro: str, build_number: int | None) -> lis def pr_summary(results: dict, distro: str, platform: str, channel: str, build_number: int | None = None) -> list[str]: - """Markdown for the pull-request comment: what needs attention as a table, the - rest collapsed. Packages without a release are left out; a distribution with - nothing to compare gets no section.""" + """Markdown for the pull-request comment: one collapsed section per distribution, + with the counts in its summary line, open only when dependents need a rebuild. + Inside, what needs attention as a table, the rest collapsed again. Packages without + a release are left out; a distribution with nothing to compare gets no section.""" def worst_first(item): verdict = item[1]["verdict"] return (-RANK.index(verdict) if verdict in RANK else 1, item[0]) @@ -518,21 +519,24 @@ def worst_first(item): if not attention and not rest: return [] - lines = [f"#### {distro} {platform}", ""] + breaking = [n for n, r in attention if r["verdict"] in ("soname", "incompatible", "error")] + counts = [f"**{len(breaking)} need their dependents rebuilt**" if breaking else "", + f"{len(attention) - len(breaking)} to review (changed headers)" if len(attention) > len(breaking) else "", + f"{len(rest)} can be bumped on their own" if rest else ""] + lines = [f"{distro} {platform}: " + f"{', '.join(c for c in counts if c)}", ""] if attention: lines += TABLE_HEADER + [_row(n, r) for n, r in attention] + [""] lines += rebuild_snippet(dict(attention), distro, build_number) if rest: - counts: dict[str, int] = {} + verdicts: dict[str, int] = {} for _, res in rest: - counts[res["verdict"]] = counts.get(res["verdict"], 0) + 1 - summary = ", ".join(f"{v}: {c}" for v, c in sorted(counts.items())) - if not attention: - lines += ["Nothing that needs a rebuild of other packages.", ""] + verdicts[res["verdict"]] = verdicts.get(res["verdict"], 0) + 1 + summary = ", ".join(f"{v}: {c}" for v, c in sorted(verdicts.items())) lines += [f"
{len(rest)} packages can be bumped on their own ({summary})", "", *TABLE_HEADER, *[_row(n, r) for n, r in rest], "", "
", ""] where = " and ".join(f"{c}/{platform}" for c in ([channel] if isinstance(channel, str) else channel)) - return lines + [f"Compared with the newest builds on {where}.", ""] + return lines + [f"Compared with the newest builds on {where}.", "", "", ""] def main() -> int: diff --git a/tools/tests/test_abi_check.py b/tools/tests/test_abi_check.py index 7787897a..cf6ed388 100644 --- a/tools/tests/test_abi_check.py +++ b/tools/tests/test_abi_check.py @@ -46,6 +46,12 @@ def test_summary_puts_the_snippet_under_the_table(): text = "\n".join(abi_check.pr_summary(results, "jazzy", "linux-64", ["https://x/test", "https://x/jazzy"], 26)) assert text.index("| foo |") < text.index("bar:\n build_number: 26") < text.index("
") assert "https://x/test/linux-64 and https://x/jazzy/linux-64" in text + # one section per distribution, open (with the count in the summary) when dependents need a rebuild + assert text.startswith("
jazzy linux-64: **1 need their dependents rebuilt**, " + "1 can be bumped on their own") + assert text.rstrip().endswith("
") + quiet = "\n".join(abi_check.pr_summary({"ok": results["ok"]}, "jazzy", "linux-64", "https://x/jazzy", 26)) + assert quiet.startswith("
jazzy linux-64: 1 can be bumped on their own") def test_next_build_number_is_above_vinca_and_every_published_build(tmp_path, monkeypatch):