This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git
The following commit(s) were added to refs/heads/main by this push:
new ff7dd1d7b ci: add apidiff check for incompatible API changes (#2166)
ff7dd1d7b is described below
commit ff7dd1d7b34e323953f6fec8647424d3079d5e33
Author: Andrei Tserakhau <[email protected]>
AuthorDate: Tue Oct 13 13:40:53 2026 +0200
ci: add apidiff check for incompatible API changes (#2166)
Part of the v1.0 freeze work in #2062. After the 1.0.0 tag, any breaking
change to the exported API needs a /v2 module, so we want CI to catch them.
The new "API compatibility" workflow runs golang.org/x/exp/cmd/apidiff in
module mode on every pull request. It exports the API of the base branch
tip the PR merges onto and compares the merged result against it. Internal
packages are skipped. The list of incompatible changes goes to the job
summary.
Until 1.0 we still make intentional breaks, so a break only fails the check
when the PR title does not declare it with the Conventional Commits "!"
marker (for example "refactor(io)!: ..."), which the repo already uses for
breaking changes. Editing the title re-runs the check. After the tag, the
title escape hatch can be dropped so any break fails.
If apidiff itself fails, for example because the base or the PR does not
build, the script prints the error and exits 2, so a broken build is not
reported as an API break.
The logic lives in dev/check-api-compat.sh so it can also be run locally
against any base ref. CONTRIBUTING.md now mentions the check and the "!"
convention.
Signed-off-by: laskoviymishka <[email protected]>
---
.github/workflows/api-compat.yml | 82 ++++++++++++++++++++++++
CONTRIBUTING.md | 1 +
dev/check-api-compat.sh | 131 +++++++++++++++++++++++++++++++++++++++
3 files changed, 214 insertions(+)
diff --git a/.github/workflows/api-compat.yml b/.github/workflows/api-compat.yml
new file mode 100644
index 000000000..16cd71b1d
--- /dev/null
+++ b/.github/workflows/api-compat.yml
@@ -0,0 +1,82 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+name: API compatibility
+
+on:
+ pull_request:
+ # edited re-runs the check when the title gains or loses its "!" marker.
+ types: [opened, synchronize, reopened, edited]
+
+concurrency:
+ group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{
github.workflow }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ apidiff:
+ name: apidiff
+ runs-on: ubuntu-latest
+ env:
+ GOMODCACHE: ${{ github.workspace }}/.gocache/mod
+ GOCACHE: ${{ github.workspace }}/.gocache/build
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ # The PR merge commit plus its first parent, the base branch tip it
+ # was merged onto, which is what the API is compared against.
+ fetch-depth: 2
+ # The Go version and cache key match go-ci.yml's ubuntu-latest / 1.27.1
+ # matrix entry, whose cache main saves; keep them in step when it changes.
+ - name: Install Go
+ uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
+ with:
+ go-version: '1.27.1'
+ cache: false
+ # Reuse the Go job's cache from main; this job never saves one.
+ - name: Restore Go module and build cache
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #
v6.1.0
+ with:
+ path: |
+ ${{ github.workspace }}/.gocache/mod
+ ${{ github.workspace }}/.gocache/build
+ key: go-ubuntu-latest-go1.27.1-${{ hashFiles('go.sum') }}
+ restore-keys: |
+ go-ubuntu-latest-go1.27.1-
+ # The version is pinned once, as APIDIFF_VERSION in the script.
+ - name: Install apidiff
+ run: |
+ version="$(sed -n 's/^APIDIFF_VERSION="\(.*\)"$/\1/p'
dev/check-api-compat.sh)"
+ if [[ -z "${version}" ]]; then
+ echo "::error::APIDIFF_VERSION not found in dev/check-api-compat.sh"
+ exit 2
+ fi
+ go install "golang.org/x/exp/cmd/apidiff@${version}"
+ - name: Check for incompatible API changes
+ env:
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ run: |
+ # HEAD^1 is only the base tip when HEAD is the PR merge commit; refuse
+ # to compare against anything else.
+ if ! git rev-parse --verify --quiet HEAD^2 >/dev/null; then
+ echo "::error::expected the pull request merge commit to be checked
out"
+ exit 2
+ fi
+ dev/check-api-compat.sh HEAD^1
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c81b48983..85efa66f3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -38,6 +38,7 @@ If two PRs land for the same issue, we will generally keep
the one from the cont
- Keep PRs focused — one issue per PR.
- Run `make test` and `make lint` before pushing. CI runs all of these too,
but catching issues locally saves a round-trip. `make lint-install` will
install the linter if you don't have it yet.
- All commits must have a `Signed-off-by` line
([DCO](https://developercertificate.org/)).
+- CI checks the exported API against the base branch with `apidiff`. If your
PR changes it incompatibly on purpose, mark the PR title as breaking with a
`!`, for example `refactor(io)!: add context to IO.Open`. You can run the same
check locally with `dev/check-api-compat.sh` after installing the `apidiff`
version it names.
## Code Review
diff --git a/dev/check-api-compat.sh b/dev/check-api-compat.sh
new file mode 100755
index 000000000..b8dc612c2
--- /dev/null
+++ b/dev/check-api-compat.sh
@@ -0,0 +1,131 @@
+#!/usr/bin/env bash
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# Reports incompatible changes to the module's exported API between a base
+# revision and the working tree, using golang.org/x/exp/cmd/apidiff.
+#
+# Usage: dev/check-api-compat.sh [BASE_REF] (default: origin/main)
+#
+# Run from the repository root. Exits 0 when the API is compatible with
+# BASE_REF. When it is not, the changes are printed and the exit status is 1,
+# unless PR_TITLE is set and marks the change as breaking with a
+# Conventional Commits "!" (for example "refactor(io)!: ..."), in which case
+# the changes are reported and the exit status is 0.
+#
+# Exit status: 0 compatible (or breaking and declared), 1 undeclared
+# incompatible changes, 2 the check itself could not run (for example the base
+# or the working tree does not build).
+#
+# When GITHUB_STEP_SUMMARY is set, the report is also appended to it.
+#
+# apidiff only sees the API for the host platform and default build tags, and
+# it does not detect behavior changes, so a pass is not a full compatibility
+# guarantee.
+
+set -euo pipefail
+
+BASE_REF="${1:-origin/main}"
+MODULE="github.com/apache/iceberg-go"
+APIDIFF="${APIDIFF:-apidiff}"
+# The apidiff version CI installs; .github/workflows/api-compat.yml reads it
+# from this line.
+APIDIFF_VERSION="v0.0.0-20261007192929-f45ad48fbe92"
+
+if ! command -v "${APIDIFF}" >/dev/null 2>&1; then
+ echo "apidiff not found; install it with:" >&2
+ echo " go install golang.org/x/exp/cmd/apidiff@${APIDIFF_VERSION}" >&2
+ exit 2
+fi
+
+work="$(mktemp -d)"
+base_dir="${work}/base"
+cleanup() {
+ git worktree remove --force "${base_dir}" >/dev/null 2>&1 || true
+ git worktree prune >/dev/null 2>&1 || true
+ rm -rf "${work}"
+}
+trap cleanup EXIT
+
+if ! git worktree add --quiet --detach "${base_dir}" "${BASE_REF}"; then
+ echo "cannot check out base ref ${BASE_REF}" >&2
+ exit 2
+fi
+
+# run_apidiff runs apidiff with its stderr captured. On failure it prints that
+# stderr, minus the "Ignoring internal package" notices apidiff always emits,
+# and exits 2 so a broken build is not mistaken for an API break.
+run_apidiff() {
+ if ! "${APIDIFF}" "$@" 2>"${work}/stderr"; then
+ echo "apidiff $* failed:" >&2
+ grep -v '^Ignoring internal package' "${work}/stderr" >&2 || true
+ exit 2
+ fi
+}
+
+# apidiff resolves the module from the current directory, so export the base
+# API from inside the base checkout and compare from the repository root.
+(cd "${base_dir}" && run_apidiff -m -w "${work}/base.export" "${MODULE}")
+run_apidiff -m -incompatible "${work}/base.export" "${MODULE}" >
"${work}/changes.txt"
+
+summary() {
+ if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
+ cat >> "${GITHUB_STEP_SUMMARY}" || true
+ else
+ cat > /dev/null
+ fi
+}
+
+if [[ ! -s "${work}/changes.txt" ]]; then
+ echo "No incompatible API changes against ${BASE_REF}."
+ echo "No incompatible API changes against \`${BASE_REF}\`." | summary
+ exit 0
+fi
+
+breaking_title='^[a-z]+(\([^)]*\))?!:'
+declared=false
+if [[ -n "${PR_TITLE:-}" && "${PR_TITLE}" =~ ${breaking_title} ]]; then
+ declared=true
+fi
+
+echo "Incompatible API changes against ${BASE_REF}:"
+cat "${work}/changes.txt"
+
+{
+ echo "### Incompatible API changes against \`${BASE_REF}\`"
+ echo
+ echo '```'
+ cat "${work}/changes.txt"
+ echo '```'
+} | summary
+
+if [[ "${declared}" == true ]]; then
+ echo
+ echo "The PR title marks this change as breaking, so this is reported but
not failed."
+ echo "The PR title marks this change as breaking, so this is reported but
not failed." | summary
+ exit 0
+fi
+
+echo
+echo "This changes the public API incompatibly. If that is intended, mark the
PR"
+echo "title as breaking with a \"!\" after the type or scope, for example"
+echo "\"refactor(io)!: add context to IO.Open\". Otherwise keep the old API
working."
+{
+ echo
+ echo "If this break is intended, mark the PR title as breaking with a \`!\`"
+ echo "after the type or scope, for example \`refactor(io)!: add context to
IO.Open\`."
+} | summary
+exit 1