cli/jsonoutput: CI check for incompatible API changes
In PR #17683, we introduced tailscale.com/cmd/tailscale/cli/jsonouput with the intention of providing “stable and versioned JSON serialisation for CLI output.” This package is also intended to provide importable structs that third-party programs, like Headscale, can use to parse this CLI output. We also want to ensure that the wire-protocol, i.e. JSON encoding, doesn’t break either.
However, there is a danger that we accidentally break the API in this package and don’t notice. I suggest we add a check for incompatible API changes to our continuous integration checks to prevent this from accidentally happening.
As of 2026-06-03, it looks like golang.org/x/exp/apidiff is still the best option for doing this check. I’m not sure that this tool looks at struct tags, we’ll want to confirm that it can prevent incompatible json: changes. The Kubernetes project has an example GitHub Actions workflow that can inspire our implementation, but we should probably constrain this check to just the APIs where we offer backwards-compatibility guarantees:
github.com/kubernetes/utils utils/.github.workflows/test.yml:
apidiff:
runs-on: ubuntu-latest
if: github.base_ref
steps:
- name: Install Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: 1.25.x
- name: Add GOBIN to PATH
run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Install dependencies
run: go install golang.org/x/exp/cmd/apidiff@latest
- name: Checkout old code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
ref: ${{ github.base_ref }}
path: "old"
- name: Checkout new code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
path: "new"
- name: APIDiff
run: ./hack/verify-apidiff.sh -d ../old
working-directory: "new"github.com/kubernetes/utils utils/hack/verify-apidiff.sh:
pushd "${dir}" >/dev/null
echo "Inspecting API of ${base}..."
go list ./... > packages.txt
for pkg in $(cat packages.txt); do
mkdir -p "${output}/${pkg}"
apidiff -w "${output}/${pkg}/apidiff.output" "${pkg}"
done
popd >/dev/null
retval=0
echo "Comparing with ${base}..."
for pkg in $(go list ./...); do
# New packages are ok
if [ ! -f "${output}/${pkg}/apidiff.output" ]; then
continue
fi
# Check for incompatible changes to previous packages
incompatible=$(apidiff -incompatible "${output}/${pkg}/apidiff.output" "${pkg}")
if [[ -n "${incompatible}" ]]; then
echo >&2 "FAIL: ${pkg} contains incompatible changes:
${incompatible}
"
retval=1
fi
done
# Check for removed packages
removed=$(comm -23 "${dir}/packages.txt" <(go list ./...))
if [[ -n "${removed}" ]]; then
echo >&2 "FAIL: removed packages:
${removed}
"
retval=1
ficc: @alexwlchan, @kradalby
Source: tailscale/tailscale