Skip to content

Commit c8ddccc

Browse files
committed
add Check API
1 parent 36d21f8 commit c8ddccc

4 files changed

Lines changed: 194 additions & 0 deletions

File tree

.github/workflows/check-api.yml

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
name: Check API
2+
3+
# Informational API diff job: it shows which public API of the Cabal
4+
# packages changes with respect to the PR base, but it is not a required
5+
# check and does not block merging.
6+
7+
on:
8+
pull_request:
9+
paths-ignore:
10+
- 'doc/**'
11+
- '**/*.md'
12+
- 'changelog.d/**'
13+
- 'release-notes/**'
14+
push:
15+
branches:
16+
- '3.*'
17+
paths-ignore:
18+
- 'doc/**'
19+
- '**/*.md'
20+
- 'changelog.d/**'
21+
- 'release-notes/**'
22+
workflow_dispatch:
23+
24+
permissions:
25+
contents: read
26+
27+
env:
28+
# The API depends on the GHC version, so both revisions are always built
29+
# with a single pinned GHC. Use the GHC used for releases, see GHC_FOR_RELEASE
30+
# in .github/workflows/validate.yml, and bump both together.
31+
GHC_VERSION: "9.10.3"
32+
33+
jobs:
34+
check-api:
35+
name: API diff ${{ matrix.package }}
36+
runs-on: ubuntu-latest
37+
timeout-minutes: 60
38+
strategy:
39+
fail-fast: false
40+
matrix:
41+
package:
42+
- Cabal-syntax
43+
- Cabal
44+
- cabal-install-solver
45+
- Cabal-hooks
46+
47+
steps:
48+
# fetch-depth: 0 is needed because packdiff checks out the base
49+
# revision itself and builds it.
50+
- uses: actions/checkout@v7
51+
with:
52+
fetch-depth: 0
53+
ref: ${{ github.event.pull_request.head.sha }}
54+
55+
- uses: haskell-actions/setup@v2
56+
id: setup-haskell
57+
with:
58+
ghc-version: ${{ env.GHC_VERSION }}
59+
cabal-version: latest
60+
61+
# runner.os isn't sufficient for binary compatible caches
62+
- name: Get runner OS/version for cache keys
63+
id: get-osver
64+
run: echo "osver=$ImageOS" >> "$GITHUB_OUTPUT"
65+
66+
- uses: actions/cache@v6
67+
with:
68+
path: ${{ steps.setup-haskell.outputs.cabal-store }}
69+
key: ${{ steps.get-osver.outputs.osver }}-check-api-${{ env.GHC_VERSION }}-${{ github.sha }}
70+
restore-keys: ${{ steps.get-osver.outputs.osver }}-check-api-${{ env.GHC_VERSION }}-
71+
72+
- name: "Work around git problem https://bugs.launchpad.net/ubuntu/+source/git/+bug/1993586 (cabal PR #8546)"
73+
run: git config --global protocol.file.allow always
74+
75+
- name: Install packdiff
76+
run: |
77+
mkdir -p "$HOME/.local/bin"
78+
cabal install packdiff --project-file=cabal.project.api --installdir="$HOME/.local/bin" --overwrite-policy=always
79+
80+
- name: Run packdiff
81+
run: |
82+
set -o pipefail
83+
case "${{ github.event_name }}" in
84+
pull_request) base="${{ github.event.pull_request.base.sha }}" ;;
85+
push) base="${{ github.event.before }}" ;;
86+
*) base="" ;;
87+
esac
88+
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ]; then
89+
base="$(git merge-base origin/master HEAD)"
90+
fi
91+
head="$(git rev-parse HEAD)"
92+
echo "Diffing ${{ matrix.package }} API: $base -> $head"
93+
packdiff diff ${{ matrix.package }} "$base" ${{ matrix.package }} "$head" | tee api-diff.txt
94+
95+
sed -n '/API Annotations/,$p' api-diff.txt > api-diff-summary.txt
96+
{
97+
echo "## API diff \`${{ matrix.package }}\`: \`$(git rev-parse --short "$base")\` -> \`$(git rev-parse --short "$head")\`"
98+
echo '```'
99+
cat api-diff-summary.txt
100+
echo '```'
101+
} >> "$GITHUB_STEP_SUMMARY"
102+
103+
# Diff entries look like "[C] Module.Name"; the annotation legend
104+
# ("[A] : Added") has a colon right after the marker and must not match.
105+
if grep -qE '^\[[ARC]\] [^ :]' api-diff.txt; then
106+
echo "::error::The API of ${{ matrix.package }} changed with respect to the base revision. See the job summary for the diff. Consider a changelog entry, a PVP version bump, and whether the change should be backported."
107+
exit 1
108+
else
109+
echo "No API changes detected in ${{ matrix.package }}."
110+
fi

CONTRIBUTING.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,43 @@ fail annoyingly once you push it. `make checks` will do these checks. The list o
174174
checks is expected to grow over time, to make it easier to avoid CI turnaround on
175175
simple problems.
176176

177+
## API diff
178+
179+
CI runs an informational "Check API" job (`.github/workflows/check-api.yml`) on
180+
every pull request. It computes the public API diff of `Cabal-syntax`, `Cabal`,
181+
`cabal-install-solver` and `Cabal-hooks` between the base revision and the head
182+
of the PR using [packdiff](https://github.com/composewell/packdiff), and prints
183+
it in the job log and the job summary. The job is **not a required check**: it
184+
does not block merging, it exists to inform you and the reviewers:
185+
186+
- whether a PR changes the public API (perhaps accidentally);
187+
- what to write in the changelog entry and which version bump PVP requires;
188+
- whether the PR is a candidate for backporting to a release branch
189+
(API-changing PRs usually are not).
190+
191+
The diff is computed in CI with a single pinned GHC and no golden files are
192+
committed, so there is nothing to update when the API changes: an API-changing
193+
PR just turns the job red.
194+
195+
To run the same diff locally:
196+
197+
```console
198+
$ make api-install # once; installs packdiff from the pinned commit in cabal.project.api
199+
$ make api-diff # all four packages, against origin/master
200+
$ make api-diff PKG=Cabal-syntax API_BASE=3.14 # one package, against a branch/tag
201+
```
202+
203+
Notes:
204+
205+
- `packdiff` literally runs `git checkout` on both revisions, so commit or stash
206+
your changes first, and don't be surprised to find the working tree left at
207+
`HEAD` (on success) or at the base revision (on failure).
208+
- The base revision must be buildable with your local GHC; very old revisions
209+
may not be.
210+
- `packdiff` derives its output from the haddock hoogle files, so it does not
211+
cover `other-modules` and does not merge the API of re-exported modules;
212+
a small amount of noise is possible on module reshuffles.
213+
177214
## QA Notes
178215

179216
Manual Quality Assurance (QA) is performed to ensure that the changes impacting

Makefile

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,37 @@ cabal-install-test-accept:
223223
rm -rf .ghc.environment.*
224224
cd cabal-testsuite && `cabal list-bin cabal-tests` --with-cabal=`cabal list-bin cabal` --hide-successes -j3 --accept ${TEST}
225225

226+
# API diff (packdiff)
227+
##############################################################################
228+
229+
# https://github.com/composewell/packdiff; the same commit is pinned in
230+
# cabal.project.api and used by the "Check API" CI job.
231+
PACKDIFF_COMMIT := 54e786de55f091cdd3b912bd72ccc0e5e252aa77
232+
233+
API_PACKAGES := Cabal-syntax Cabal cabal-install-solver Cabal-hooks
234+
API_BASE ?= origin/master
235+
236+
.PHONY: api-install
237+
api-install: ## Install the packdiff tool used for API diffing.
238+
mkdir -p $(HOME)/.local/bin
239+
cabal install packdiff --project-file=cabal.project.api --installdir=$(HOME)/.local/bin --overwrite-policy=always
240+
241+
.PHONY: api-diff
242+
api-diff: ## API diff of PKG (default: all library packages) between API_BASE (default: origin/master) and HEAD.
243+
@command -v packdiff >/dev/null || { echo "packdiff not found; run 'make api-install'"; exit 1; }
244+
@# NB: packdiff literally runs 'git checkout' on the given revisions, so:
245+
@# * revisions are resolved to SHAs *before* running (a literal "HEAD"
246+
@# would be re-resolved after switching to the base revision);
247+
@# * the worktree must be clean (commit or stash first);
248+
@# * the base revision must be buildable with the local GHC;
249+
@# * on success the worktree is left at HEAD, on failure at the base.
250+
@base=$$(git rev-parse $(API_BASE)); \
251+
head=$$(git rev-parse HEAD); \
252+
for pkg in $(if $(PKG),$(PKG),$(API_PACKAGES)); do \
253+
echo "== packdiff $$pkg: $(API_BASE) -> HEAD =="; \
254+
packdiff diff $$pkg $$base $$pkg $$head; \
255+
done
256+
226257
# Docker validation
227258

228259
# Use this carefully, on big machine you can say

cabal.project.api

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- Project file for installing `packdiff`, the API diff tool used by
2+
-- `make api-diff` and the "Check API" CI job
3+
-- (see .github/workflows/check-api.yml).
4+
--
5+
-- packdiff is not on Hackage yet, so it is installed from a pinned
6+
-- commit of https://github.com/composewell/packdiff.
7+
--
8+
-- This repository has no root .cabal file, so the project needs an
9+
-- explicit (empty in practice) packages declaration.
10+
11+
optional-packages: .
12+
13+
source-repository-package
14+
type: git
15+
location: https://github.com/composewell/packdiff.git
16+
tag: 54e786de55f091cdd3b912bd72ccc0e5e252aa77

0 commit comments

Comments
 (0)