diff --git a/.github/scripts/check-upstream-compatibility.sh b/.github/scripts/check-upstream-compatibility.sh new file mode 100644 index 0000000..5b90450 --- /dev/null +++ b/.github/scripts/check-upstream-compatibility.sh @@ -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 diff --git a/.github/workflows/upstream-compatibility.yml b/.github/workflows/upstream-compatibility.yml new file mode 100644 index 0000000..61abbda --- /dev/null +++ b/.github/workflows/upstream-compatibility.yml @@ -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 => [ + `${job.name}`, + 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.'); + } diff --git a/README.md b/README.md index 54375b6..d27dba9 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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