Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/scripts/check-upstream-compatibility.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause

set -euo pipefail

cd "$(dirname "$0")/../.."

# Fail early if a job did not receive the exact dependency update being tested.
actual=$(go list -m -f '{{.Version}}' tailscale.com)
printf 'tailscale.com=%s (expected %s)\n' "$actual" "$TAILSCALE_VERSION"
test "$actual" = "$TAILSCALE_VERSION"
go version
go env GOOS GOARCH CGO_ENABLED

# The Swift make recipes pipe xcodebuild through a formatter. Propagate failures
# through those pipelines and through recursive make invocations.
swift_make() {
# Put flags in SHELL for compatibility with macOS's GNU make 3.81.
make -C swift 'SHELL=/bin/bash -eu -o pipefail' "$@"
}

swift_sample() {
xcodebuild build \
-project swift/Examples/TailscaleKitHello/TailscaleKitHello.xcodeproj \
-scheme "$1" \
-configuration Release \
-derivedDataPath "swift/sample-build-$2" \
-destination "$3" \
CODE_SIGNING_ALLOWED=NO
}

case "${1:?expected a compatibility suite}" in
go-c)
go test -v -count=1 -timeout 15m ./...
make c-archive shared
cc -Wall -Wextra -o "$RUNNER_TEMP/libtailscale-echo" \
example/echo_server.c libtailscale.a -lpthread -ldl -lm
;;
ruby)
(cd ruby && bundle exec rake build test)
ruby -c ruby/bin/echo_server
;;
python)
# Use the binding's documented, pinned pybind11 checkout and build path.
make -C python build
python -m pip wheel --no-deps ./python --wheel-dir "$RUNNER_TEMP/wheels"
# Run outside python/ so the import resolves to the installed package.
python -c 'from tailscale import TSNet; assert callable(TSNet)'
python -m py_compile python/examples/echo.py
;;
source-package)
make -C sourcepkg check
;;
swift-tests)
test "$(uname -m)" = arm64
swift_make test
;;
swift-macos)
test "$(uname -m)" = arm64
swift_make macos
swift_sample HelloFromTailscale_macOS macos 'platform=macOS,arch=arm64'
;;
swift-ios)
test "$(uname -m)" = arm64
swift_make ios-fat
swift_sample 'HelloFromTailscale iOS' ios 'generic/platform=iOS'
swift_sample 'HelloFromTailscale iOS' ios-sim 'generic/platform=iOS Simulator'
;;
*)
echo "Unknown compatibility suite: $1" >&2
exit 2
;;
esac
225 changes: 225 additions & 0 deletions .github/workflows/upstream-compatibility.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
name: Upstream Tailscale compatibility

on:
schedule:
- cron: '17 9 * * 1'
workflow_dispatch:
pull_request:
paths:
- '.github/workflows/upstream-compatibility.yml'
- '.github/scripts/check-upstream-compatibility.sh'

# This workflow only changes disposable runner checkouts.
permissions:
contents: read

concurrency:
group: upstream-compatibility-${{ github.ref }}
cancel-in-progress: true

jobs:
prepare:
name: Prepare latest stable Tailscale
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
version: ${{ steps.release.outputs.version }}
current: ${{ steps.release.outputs.current }}
changed: ${{ steps.release.outputs.changed }}
go-version: ${{ steps.update.outputs.go-version }}
steps:
- uses: actions/checkout@v4

- name: Find latest stable release
id: release
uses: actions/github-script@v7
with:
script: |
const { readFileSync } = require('node:fs');
const current = readFileSync('go.mod', 'utf8').match(/^\s*(?:require\s+)?tailscale\.com\s+(v[^\s]+)(?:\s|$)/m);
if (!current) throw new Error('tailscale.com dependency missing from go.mod');
core.setOutput('current', current[1]);
const { data: release } = await github.rest.repos.getLatestRelease({
owner: 'tailscale', repo: 'tailscale',
});
if (release.draft || release.prerelease || !/^v\d+\.\d+\.\d+$/.test(release.tag_name)) {
throw new Error(`Unexpected stable release: ${release.tag_name}`);
}
core.setOutput('version', release.tag_name);
core.setOutput('changed', release.tag_name !== current[1]);
core.info(`Latest stable: ${release.tag_name}; checked in: ${current[1]}`);

- uses: actions/setup-go@v5
if: steps.release.outputs.changed == 'true'
with:
go-version-file: go.mod

- name: Update disposable checkout
id: update
if: steps.release.outputs.changed == 'true'
env:
TAILSCALE_VERSION: ${{ steps.release.outputs.version }}
GOTOOLCHAIN: auto
shell: bash
run: |
mkdir -p compatibility-logs
{
current=$(go list -m -f '{{.Version}}' tailscale.com)
echo "Updating tailscale.com from $current to $TAILSCALE_VERSION"
go get "tailscale.com@$TAILSCALE_VERSION"
go mod tidy
actual=$(go list -m -f '{{.Version}}' tailscale.com)
test "$actual" = "$TAILSCALE_VERSION"
go version
echo "go-version=$(go env GOVERSION)" >> "$GITHUB_OUTPUT"
git diff -- go.mod go.sum
} 2>&1 | tee compatibility-logs/update.log

- name: Share exact candidate dependencies
if: steps.release.outputs.changed == 'true'
uses: actions/upload-artifact@v4
with:
name: compatibility-dependencies
path: |
go.mod
go.sum
retention-days: 14
if-no-files-found: error

- name: Save update diagnostics
if: always() && steps.release.outputs.changed == 'true'
uses: actions/upload-artifact@v4
with:
name: compatibility-update-logs
path: compatibility-logs/
retention-days: 14
if-no-files-found: ignore

check:
name: Compatibility / ${{ matrix.suite }}
needs: prepare
if: needs.prepare.outputs.changed == 'true'
strategy:
fail-fast: false
matrix:
include:
- suite: go-c
runner: ubuntu-latest
- suite: ruby
runner: ubuntu-latest
- suite: python
runner: ubuntu-latest
- suite: source-package
runner: ubuntu-latest
- suite: swift-tests
runner: macos-latest
- suite: swift-macos
runner: macos-latest
- suite: swift-ios
runner: macos-latest
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
env:
CGO_ENABLED: '1'
GOTOOLCHAIN: auto
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
name: compatibility-dependencies

# Read the updated go.mod: upstream may require a newer Go toolchain.
- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Set up Ruby
if: matrix.suite == 'ruby'
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.4'
bundler-cache: true
working-directory: ruby

- name: Set up Python
if: matrix.suite == 'python'
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Build and test candidate
env:
SUITE: ${{ matrix.suite }}
TAILSCALE_VERSION: ${{ needs.prepare.outputs.version }}
shell: bash
run: |
mkdir -p compatibility-logs
bash .github/scripts/check-upstream-compatibility.sh "$SUITE" \
2>&1 | tee "compatibility-logs/$SUITE.log"

- name: Save logs and Xcode test results
if: always()
uses: actions/upload-artifact@v4
with:
name: compatibility-${{ matrix.suite }}-logs
path: |
compatibility-logs/
swift/build/Logs/Test/
retention-days: 14
if-no-files-found: ignore

report:
name: Compatibility report
if: always()
needs: [prepare, check]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: read
steps:
- name: Summarize every check
uses: actions/github-script@v7
env:
TAILSCALE_VERSION: ${{ needs.prepare.outputs.version }}
CURRENT_VERSION: ${{ needs.prepare.outputs.current }}
GO_VERSION: ${{ needs.prepare.outputs.go-version }}
CHANGED: ${{ needs.prepare.outputs.changed }}
PREPARE_RESULT: ${{ needs.prepare.result }}
CHECK_RESULT: ${{ needs.check.result }}
RUN_ATTEMPT: ${{ github.run_attempt }}
with:
script: |
if (process.env.PREPARE_RESULT === 'success' && process.env.CHANGED === 'false') {
await core.summary
.addHeading('Upstream Tailscale compatibility')
.addRaw(`**PASS** — already at ${process.env.TAILSCALE_VERSION}; builds skipped.\n`)
.write();
return;
}
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRunAttempt, {
...context.repo,
run_id: context.runId,
attempt_number: Number(process.env.RUN_ATTEMPT),
per_page: 100,
});
const checks = jobs.filter(job => job.name !== 'Compatibility report');
const rows = checks.map(job => [
`<a href="${job.html_url}">${job.name}</a>`,
job.conclusion || job.status,
]);
const passed = process.env.PREPARE_RESULT === 'success' && process.env.CHECK_RESULT === 'success';
await core.summary
.addHeading('Upstream Tailscale compatibility')
.addRaw(`**${passed ? 'PASS' : 'FAIL / INCOMPLETE'}**\n\n`)
.addRaw(`Candidate: ${process.env.TAILSCALE_VERSION || 'release discovery failed'}\n\n`)
.addRaw(`Checked-in dependency: ${process.env.CURRENT_VERSION || 'unavailable'}\n\n`)
.addRaw(`Go toolchain: ${process.env.GO_VERSION || 'unavailable'}\n\n`)
.addRaw(`Commit: \`${context.sha}\`\n\n`)
.addTable([[{data: 'Check', header: true}, {data: 'Result', header: true}], ...rows])
.addRaw('\nBuild logs, dependency files, and available Xcode test results are attached as run artifacts. Setup failures are recorded in the linked job logs.\n')
.addRaw('\nSample apps are built without signing or tailnet credentials; they are not exercised against a live tailnet.\n')
.write();
if (!passed) {
core.setFailed('Upstream compatibility checks failed or did not complete. See the job summary and artifacts.');
}
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# libtailscale

[![Upstream Tailscale compatibility](https://github.com/tailscale/libtailscale/actions/workflows/upstream-compatibility.yml/badge.svg?branch=main)](https://github.com/tailscale/libtailscale/actions/workflows/upstream-compatibility.yml)

libtailscale is a C library that embeds Tailscale into a process.

Use this library to compile Tailscale into your program and get
Expand Down Expand Up @@ -34,6 +36,13 @@ or
make shared
```

## Upstream compatibility checks

The **Upstream Tailscale compatibility** GitHub Action runs weekly or manually
to test the library, bindings, and samples against the latest stable Tailscale
release. Matching versions skip the builds and report success. See the workflow
run for results and logs.

## Bugs

Please file any issues about this code or the hosted service on
Expand Down
Loading