#19985·tailscale

cli/jsonoutput: CI check for incompatible API changes

Author: sfllawCreated Jun 3, 2026Updated Sep 30, 2026
Labelsgood first issuetestingapicli

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:

yaml
  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:

bash
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
fi

cc: @alexwlchan, @kradalby