mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-15 07:20:36 -04:00
Also reports when shard results are missing, unreadable, duplicated, or empty, so the pass count and percentage can't overstate what actually ran. Updates #20931 Signed-off-by: Yaruk Asghar <yaruk@tailscale.com>
1117 lines
44 KiB
YAML
1117 lines
44 KiB
YAML
# This is our main "CI tests" workflow. It runs everything that should run on
|
|
# both PRs and merged commits, and for the latter reports failures to slack.
|
|
name: CI
|
|
|
|
env:
|
|
# Our fuzz job, powered by OSS-Fuzz, fails periodically because we upgrade to
|
|
# new Go versions very eagerly. OSS-Fuzz is a little more conservative, and
|
|
# ends up being unable to compile our code.
|
|
#
|
|
# When this happens, we want to disable the fuzz target until OSS-Fuzz catches
|
|
# up. However, we also don't want to forget to turn it back on when OSS-Fuzz
|
|
# can once again build our code.
|
|
#
|
|
# This variable toggles the fuzz job between two modes:
|
|
# - false: we expect fuzzing to be happy, and should report failure if it's not.
|
|
# - true: we expect fuzzing is broken, and should report failure if it start working.
|
|
TS_FUZZ_CURRENTLY_BROKEN: false
|
|
# GOMODCACHE is the same definition on all OSes. Within the workspace, we use
|
|
# toplevel directories "src" (for the checked out source code), and "gomodcache"
|
|
# and other caches as siblings to follow.
|
|
GOMODCACHE: ${{ github.workspace }}/gomodcache
|
|
CMD_GO_USE_GIT_HASH: "true"
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- "main"
|
|
- "release-branch/*"
|
|
pull_request:
|
|
# all PRs on all branches
|
|
merge_group:
|
|
branches:
|
|
- "main"
|
|
|
|
concurrency:
|
|
# For PRs, later CI runs preempt previous ones. e.g. a force push on a PR
|
|
# cancels running CI jobs and starts all new ones.
|
|
#
|
|
# For non-PR pushes, concurrency.group needs to be unique for every distinct
|
|
# CI run we want to have happen. Use run_id, which in practice means all
|
|
# non-PR CI runs will be allowed to run without preempting each other.
|
|
group: ${{ github.workflow }}-$${{ github.pull_request.number || github.run_id }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
gomod-cache:
|
|
runs-on: ubuntu-24.04
|
|
outputs:
|
|
cache-key: ${{ steps.hash.outputs.key }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Compute cache key from go.{mod,sum}
|
|
id: hash
|
|
run: echo "key=gomod-cross3-${{ hashFiles('src/go.mod', 'src/go.sum') }}" >> $GITHUB_OUTPUT
|
|
# See if the cache entry already exists to avoid downloading it
|
|
# and doing the cache write again.
|
|
- id: check-cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache # relative to workspace; see env note at top of file
|
|
key: ${{ steps.hash.outputs.key }}
|
|
lookup-only: true
|
|
enableCrossOsArchive: true
|
|
- name: Download modules
|
|
if: steps.check-cache.outputs.cache-hit != 'true'
|
|
working-directory: src
|
|
run: go mod download
|
|
- name: Cache Go modules
|
|
if: steps.check-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # zizmor: ignore[cache-poisoning] v5.0.4
|
|
with:
|
|
path: gomodcache # relative to workspace; see env note at top of file
|
|
key: ${{ steps.hash.outputs.key }}
|
|
enableCrossOsArchive: true
|
|
|
|
race-root-integration:
|
|
runs-on: ubuntu-24.04
|
|
needs: gomod-cache
|
|
strategy:
|
|
fail-fast: false # don't abort the entire matrix if one element fails
|
|
matrix:
|
|
include:
|
|
- shard: '1/4'
|
|
- shard: '2/4'
|
|
- shard: '3/4'
|
|
- shard: '4/4'
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: build test wrapper
|
|
working-directory: src
|
|
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
|
|
- name: integration tests as root
|
|
working-directory: src
|
|
run: PATH=$PWD/tool:$PATH /tmp/testwrapper -exec "sudo -E" -race ./tstest/integration/
|
|
env:
|
|
TS_TEST_SHARD: ${{ matrix.shard }}
|
|
|
|
test:
|
|
strategy:
|
|
fail-fast: false # don't abort the entire matrix if one element fails
|
|
matrix:
|
|
include:
|
|
- goarch: amd64
|
|
- goarch: amd64
|
|
buildflags: "-race"
|
|
shard: '1/3'
|
|
- goarch: amd64
|
|
buildflags: "-race"
|
|
shard: '2/3'
|
|
- goarch: amd64
|
|
buildflags: "-race"
|
|
shard: '3/3'
|
|
- goarch: "386" # thanks yaml
|
|
runs-on: ubuntu-24.04
|
|
needs: gomod-cache
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: Restore Cache
|
|
id: restore-cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
# Note: this is only restoring the build cache. Mod cache is shared amongst
|
|
# all jobs in the workflow.
|
|
path: |
|
|
~/.cache/go-build
|
|
~\AppData\Local\go-build
|
|
key: ${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-${{ matrix.shard }}-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
|
|
restore-keys: |
|
|
${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-${{ matrix.shard }}-${{ hashFiles('**/go.sum') }}-${{ github.job }}-
|
|
${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-${{ matrix.shard }}-${{ hashFiles('**/go.sum') }}-
|
|
${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-${{ matrix.shard }}-
|
|
${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-
|
|
- name: build all
|
|
if: matrix.buildflags == '' # skip on race builder
|
|
working-directory: src
|
|
run: ./tool/go build ${{matrix.buildflags}} ./...
|
|
env:
|
|
GOARCH: ${{ matrix.goarch }}
|
|
- name: build variant CLIs
|
|
if: matrix.buildflags == '' # skip on race builder
|
|
working-directory: src
|
|
run: |
|
|
./build_dist.sh --extra-small ./cmd/tailscaled
|
|
./build_dist.sh --box ./cmd/tailscaled
|
|
./build_dist.sh --extra-small --box ./cmd/tailscaled
|
|
rm -f tailscaled
|
|
env:
|
|
GOARCH: ${{ matrix.goarch }}
|
|
- name: get qemu # for tstest/archtest
|
|
if: matrix.goarch == 'amd64' && matrix.buildflags == ''
|
|
run: |
|
|
sudo apt-get -y update
|
|
sudo apt-get -y install qemu-user
|
|
- name: build test wrapper
|
|
working-directory: src
|
|
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
|
|
- name: test all
|
|
working-directory: src
|
|
run: NOBASHDEBUG=true NOPWSHDEBUG=true PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}}
|
|
env:
|
|
GOARCH: ${{ matrix.goarch }}
|
|
TS_TEST_SHARD: ${{ matrix.shard }}
|
|
- name: bench all
|
|
working-directory: src
|
|
run: ./tool/go test ${{matrix.buildflags}} -bench=. -benchtime=1x -run='^$' $(for x in $(git grep -l '^func Benchmark' | xargs dirname | sort | uniq); do echo "./$x"; done)
|
|
env:
|
|
GOARCH: ${{ matrix.goarch }}
|
|
- name: check that no tracked files changed
|
|
working-directory: src
|
|
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
|
|
- name: check that no new files were added
|
|
working-directory: src
|
|
run: |
|
|
# Note: The "error: pathspec..." you see below is normal!
|
|
# In the success case in which there are no new untracked files,
|
|
# git ls-files complains about the pathspec not matching anything.
|
|
# That's OK. It's not worth the effort to suppress. Please ignore it.
|
|
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
|
|
then
|
|
echo "Build/test created untracked files in the repo (file names above)."
|
|
exit 1
|
|
fi
|
|
- name: Tidy cache
|
|
working-directory: src
|
|
shell: bash
|
|
run: |
|
|
find $(go env GOCACHE) -type f -mmin +90 -delete
|
|
- name: Save Cache
|
|
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
|
|
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
|
|
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
# Note: this is only saving the build cache. Mod cache is shared amongst
|
|
# all jobs in the workflow.
|
|
path: |
|
|
~/.cache/go-build
|
|
~\AppData\Local\go-build
|
|
key: ${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-${{ matrix.shard }}-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
|
|
|
|
windows:
|
|
permissions:
|
|
id-token: write # This is required for requesting the GitHub action identity JWT that can auth to cigocached
|
|
contents: read # This is required for actions/checkout
|
|
# ci-windows-github-1 is a 2022 GitHub-managed runner in our org with 8 cores
|
|
# and 32 GB of RAM. It is connected to a private Azure VNet that hosts cigocached.
|
|
# https://github.com/organizations/tailscale/settings/actions/github-hosted-runners/5
|
|
runs-on: ci-windows-github-1
|
|
needs: gomod-cache
|
|
name: Windows (${{ matrix.name || matrix.shard}})
|
|
strategy:
|
|
fail-fast: false # don't abort the entire matrix if one element fails
|
|
matrix:
|
|
include:
|
|
- key: "win-bench"
|
|
name: "benchmarks"
|
|
- key: "win-shard-1-2"
|
|
shard: "1/2"
|
|
- key: "win-shard-2-2"
|
|
shard: "2/2"
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: ${{ github.workspace }}/src
|
|
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
|
|
- name: Set up cigocacher
|
|
id: cigocacher-setup
|
|
uses: ./src/.github/actions/go-cache
|
|
with:
|
|
checkout-path: ${{ github.workspace }}/src
|
|
cache-dir: ${{ github.workspace }}/cigocacher
|
|
cigocached-url: ${{ vars.CIGOCACHED_AZURE_URL }}
|
|
cigocached-host: ${{ vars.CIGOCACHED_AZURE_HOST }}
|
|
|
|
- name: test
|
|
shell: bash
|
|
if: matrix.key != 'win-bench' # skip on bench builder
|
|
working-directory: src
|
|
# -count=1: a cached pass is never retried, so flakes can't be reported.
|
|
run: ./tool/go run ./cmd/testwrapper sharded:${{ matrix.shard }} -count=1
|
|
env:
|
|
NOPWSHDEBUG: "true" # to quiet tool/gocross/gocross-wrapper.ps1 in CI
|
|
TS_TESTWRAPPER_RESULTS_SUMMARY: "1"
|
|
TS_TESTWRAPPER_RESULTS_PKG: tailscale.com/tstest/integration
|
|
TS_TESTWRAPPER_RESULTS_JSON: ${{ github.workspace }}/windows-integration-results.json
|
|
|
|
- name: Upload Windows integration results
|
|
if: always() && matrix.key != 'win-bench'
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: windows-integration-results-${{ matrix.key }}
|
|
path: ${{ github.workspace }}/windows-integration-results.json
|
|
if-no-files-found: ignore
|
|
|
|
- name: bench all
|
|
shell: bash
|
|
if: matrix.key == 'win-bench'
|
|
working-directory: src
|
|
run: ./tool/go test -bench=. -benchtime=1x -run='^$' $(for x in $(git grep -l '^func Benchmark' | xargs dirname | sort | uniq); do echo "./$x"; done)
|
|
env:
|
|
NOPWSHDEBUG: "true" # to quiet tool/gocross/gocross-wrapper.ps1 in CI
|
|
|
|
- name: Print stats
|
|
shell: pwsh
|
|
if: steps.cigocacher-setup.outputs.success == 'true'
|
|
env:
|
|
GOCACHEPROG: ${{ env.GOCACHEPROG }}
|
|
run: |
|
|
Invoke-Expression "$env:GOCACHEPROG --stats" | jq .
|
|
|
|
macos:
|
|
runs-on: macos-latest
|
|
needs: gomod-cache
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: Restore Cache
|
|
id: restore-cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: ~/Library/Caches/go-build
|
|
key: ${{ runner.os }}-go-test-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
|
|
restore-keys: |
|
|
${{ runner.os }}-go-test-${{ hashFiles('**/go.sum') }}-${{ github.job }}-
|
|
${{ runner.os }}-go-test-${{ hashFiles('**/go.sum') }}-
|
|
${{ runner.os }}-go-test-
|
|
- name: build test wrapper
|
|
working-directory: src
|
|
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
|
|
- name: test all
|
|
working-directory: src
|
|
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./...
|
|
- name: check that no tracked files changed
|
|
working-directory: src
|
|
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
|
|
- name: check that no new files were added
|
|
working-directory: src
|
|
run: |
|
|
# Note: The "error: pathspec..." you see below is normal!
|
|
# In the success case in which there are no new untracked files,
|
|
# git ls-files complains about the pathspec not matching anything.
|
|
# That's OK. It's not worth the effort to suppress. Please ignore it.
|
|
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
|
|
then
|
|
echo "Build/test created untracked files in the repo (file names above)."
|
|
exit 1
|
|
fi
|
|
- name: Tidy cache
|
|
working-directory: src
|
|
run: |
|
|
find $(./tool/go env GOCACHE) -type f -mmin +90 -delete
|
|
- name: Save Cache
|
|
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
|
|
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
|
|
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: ~/Library/Caches/go-build
|
|
key: ${{ runner.os }}-go-test-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
|
|
|
|
privileged:
|
|
needs: gomod-cache
|
|
runs-on: ubuntu-24.04
|
|
container:
|
|
image: golang:latest # zizmor: ignore[unpinned-images]
|
|
options: --privileged
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: chown
|
|
working-directory: src
|
|
run: chown -R $(id -u):$(id -g) $PWD
|
|
- name: privileged tests
|
|
working-directory: src
|
|
run: ./tool/go test $(./tool/go run ./tool/listpkgs --has-root-tests)
|
|
|
|
cross: # cross-compile checks, build only.
|
|
needs: gomod-cache
|
|
strategy:
|
|
fail-fast: false # don't abort the entire matrix if one element fails
|
|
matrix:
|
|
include:
|
|
# Note: linux/amd64 is not in this matrix, because that goos/goarch is
|
|
# tested more exhaustively in the 'test' job above.
|
|
- goos: linux
|
|
goarch: arm64
|
|
- goos: linux
|
|
goarch: "386" # thanks yaml
|
|
- goos: linux
|
|
goarch: loong64
|
|
- goos: linux
|
|
goarch: arm
|
|
goarm: "5"
|
|
- goos: linux
|
|
goarch: arm
|
|
goarm: "7"
|
|
# macOS
|
|
- goos: darwin
|
|
goarch: amd64
|
|
- goos: darwin
|
|
goarch: arm64
|
|
# Windows
|
|
- goos: windows
|
|
goarch: amd64
|
|
- goos: windows
|
|
goarch: arm64
|
|
# BSDs
|
|
- goos: freebsd
|
|
goarch: amd64
|
|
- goos: openbsd
|
|
goarch: amd64
|
|
|
|
runs-on: ubuntu-24.04
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: Restore Cache
|
|
id: restore-cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
# Note: this is only restoring the build cache. Mod cache is shared amongst
|
|
# all jobs in the workflow.
|
|
path: |
|
|
~/.cache/go-build
|
|
~\AppData\Local\go-build
|
|
key: ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.goarm }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
|
|
restore-keys: |
|
|
${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.goarm }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-
|
|
${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.goarm }}-go-${{ hashFiles('**/go.sum') }}-
|
|
${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.goarm }}-go-
|
|
- name: build all
|
|
working-directory: src
|
|
run: ./tool/go build ./cmd/...
|
|
env:
|
|
GOOS: ${{ matrix.goos }}
|
|
GOARCH: ${{ matrix.goarch }}
|
|
GOARM: ${{ matrix.goarm }}
|
|
CGO_ENABLED: "0"
|
|
- name: build tests
|
|
working-directory: src
|
|
run: ./tool/go test -exec=true ./...
|
|
env:
|
|
GOOS: ${{ matrix.goos }}
|
|
GOARCH: ${{ matrix.goarch }}
|
|
CGO_ENABLED: "0"
|
|
- name: Tidy cache
|
|
working-directory: src
|
|
shell: bash
|
|
run: |
|
|
find $(go env GOCACHE) -type f -mmin +90 -delete
|
|
- name: Save Cache
|
|
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
|
|
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
|
|
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
# Note: this is only saving the build cache. Mod cache is shared amongst
|
|
# all jobs in the workflow.
|
|
path: |
|
|
~/.cache/go-build
|
|
~\AppData\Local\go-build
|
|
key: ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.goarm }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
|
|
|
|
ios: # similar to cross above, but iOS can't build most of the repo. So, just
|
|
# make it build a few smoke packages.
|
|
runs-on: ubuntu-24.04
|
|
needs: gomod-cache
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: build some
|
|
working-directory: src
|
|
run: ./tool/go build ./ipn/... ./ssh/tailssh ./wgengine/ ./types/... ./control/controlclient
|
|
env:
|
|
GOOS: ios
|
|
GOARCH: arm64
|
|
|
|
crossmin: # cross-compile for platforms where we only check cmd/tailscale{,d}
|
|
needs: gomod-cache
|
|
strategy:
|
|
fail-fast: false # don't abort the entire matrix if one element fails
|
|
matrix:
|
|
include:
|
|
# Plan9
|
|
- goos: plan9
|
|
goarch: amd64
|
|
# AIX
|
|
- goos: aix
|
|
goarch: ppc64
|
|
# Solaris
|
|
- goos: solaris
|
|
goarch: amd64
|
|
# illumos
|
|
- goos: illumos
|
|
goarch: amd64
|
|
|
|
runs-on: ubuntu-24.04
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: Restore Cache
|
|
id: restore-cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
# Note: this is only restoring the build cache. Mod cache is shared amongst
|
|
# all jobs in the workflow.
|
|
path: |
|
|
~/.cache/go-build
|
|
~\AppData\Local\go-build
|
|
key: ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
|
|
restore-keys: |
|
|
${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-
|
|
${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-${{ hashFiles('**/go.sum') }}-
|
|
${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-
|
|
- name: build core
|
|
working-directory: src
|
|
run: ./tool/go build ./cmd/tailscale ./cmd/tailscaled
|
|
env:
|
|
GOOS: ${{ matrix.goos }}
|
|
GOARCH: ${{ matrix.goarch }}
|
|
GOARM: ${{ matrix.goarm }}
|
|
CGO_ENABLED: "0"
|
|
- name: Tidy cache
|
|
working-directory: src
|
|
shell: bash
|
|
run: |
|
|
find $(go env GOCACHE) -type f -mmin +90 -delete
|
|
- name: Save Cache
|
|
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
|
|
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
|
|
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
# Note: this is only saving the build cache. Mod cache is shared amongst
|
|
# all jobs in the workflow.
|
|
path: |
|
|
~/.cache/go-build
|
|
~\AppData\Local\go-build
|
|
key: ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
|
|
|
|
android:
|
|
# similar to cross above, but android fails to build a few pieces of the
|
|
# repo. We should fix those pieces, they're small, but as a stepping stone,
|
|
# only test the subset of android that our past smoke test checked.
|
|
runs-on: ubuntu-24.04
|
|
needs: gomod-cache
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
# Super minimal Android build that doesn't even use CGO and doesn't build everything that's needed
|
|
# and is only arm64. But it's a smoke build: it's not meant to catch everything. But it'll catch
|
|
# some Android breakages early.
|
|
# TODO(bradfitz): better; see https://github.com/tailscale/tailscale/issues/4482
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: build some
|
|
working-directory: src
|
|
run: ./tool/go install ./net/netns ./ipn/ipnlocal ./wgengine/magicsock/ ./wgengine/ ./wgengine/router/ ./wgengine/netstack ./util/dnsname/ ./ipn/ ./net/netmon ./wgengine/router/ ./tailcfg/ ./types/logger/ ./net/dns ./hostinfo ./version ./ssh/tailssh
|
|
env:
|
|
GOOS: android
|
|
GOARCH: arm64
|
|
|
|
wasm: # builds tsconnect, which is the only wasm build we support
|
|
runs-on: ubuntu-24.04
|
|
needs: gomod-cache
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: Restore Cache
|
|
id: restore-cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
# Note: this is only restoring the build cache. Mod cache is shared amongst
|
|
# all jobs in the workflow.
|
|
path: |
|
|
~/.cache/go-build
|
|
~\AppData\Local\go-build
|
|
key: ${{ runner.os }}-js-wasm-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
|
|
restore-keys: |
|
|
${{ runner.os }}-js-wasm-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-
|
|
${{ runner.os }}-js-wasm-go-${{ hashFiles('**/go.sum') }}-
|
|
${{ runner.os }}-js-wasm-go-
|
|
- name: build tsconnect client
|
|
working-directory: src
|
|
run: ./tool/go build ./cmd/tsconnect/wasm ./cmd/tailscale/cli
|
|
env:
|
|
GOOS: js
|
|
GOARCH: wasm
|
|
- name: build tsconnect server
|
|
working-directory: src
|
|
# Note, no GOOS/GOARCH in env on this build step, we're running a build
|
|
# tool that handles the build itself.
|
|
run: |
|
|
./tool/go run ./cmd/tsconnect --fast-compression build
|
|
./tool/go run ./cmd/tsconnect --fast-compression build-pkg
|
|
- name: verify Google Chrome is available
|
|
run: |
|
|
which google-chrome
|
|
google-chrome --version
|
|
- name: tsconnect js/wasm headless-browser tests
|
|
working-directory: src
|
|
run: ./tool/go test ./tstest/integration/jswasmtest/ -v -timeout 180s --run-headless-browser-tests
|
|
- name: Tidy cache
|
|
working-directory: src
|
|
shell: bash
|
|
run: |
|
|
find $(go env GOCACHE) -type f -mmin +90 -delete
|
|
- name: Save Cache
|
|
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
|
|
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
|
|
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
# Note: this is only saving the build cache. Mod cache is shared amongst
|
|
# all jobs in the workflow.
|
|
path: |
|
|
~/.cache/go-build
|
|
~\AppData\Local\go-build
|
|
key: ${{ runner.os }}-js-wasm-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
|
|
|
|
tailscale_go: # Subset of tests that depend on our custom Go toolchain.
|
|
runs-on: ubuntu-24.04
|
|
needs: gomod-cache
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- name: Set GOMODCACHE env
|
|
run: echo "GOMODCACHE=$HOME/.cache/go-mod" >> $GITHUB_ENV
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: test tailscale_go
|
|
run: ./tool/go test -tags=tailscale_go,ts_enable_sockstats ./net/sockstats/...
|
|
|
|
|
|
fuzz:
|
|
# This target periodically breaks (see TS_FUZZ_CURRENTLY_BROKEN at the top
|
|
# of the file), so it's more complex than usual: the 'build fuzzers' step
|
|
# might fail, and depending on the value of 'TS_FUZZ_CURRENTLY_BROKEN', that
|
|
# might or might not be fine. The steps after the build figure out whether
|
|
# the success/failure is expected, and appropriately pass/fail the job
|
|
# overall accordingly.
|
|
#
|
|
# Practically, this means that all steps after 'build fuzzers' must have an
|
|
# explicit 'if' condition, because the default condition for steps is
|
|
# 'success()', meaning "only run this if no previous steps failed".
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: ubuntu-24.04
|
|
steps:
|
|
- name: build fuzzers
|
|
id: build
|
|
# As of 12 February 2026, this repo doesn't tag releases, so this commit
|
|
# hash is just the tip of master.
|
|
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@f277aafb36f358582fdb24a41a9a52f2e097a2fd
|
|
# continue-on-error makes steps.build.conclusion be 'success' even if
|
|
# steps.build.outcome is 'failure'. This means this step does not
|
|
# contribute to the job's overall pass/fail evaluation.
|
|
continue-on-error: true
|
|
with:
|
|
oss-fuzz-project-name: 'tailscale'
|
|
dry-run: false
|
|
language: go
|
|
- name: report unexpectedly broken fuzz build
|
|
if: steps.build.outcome == 'failure' && env.TS_FUZZ_CURRENTLY_BROKEN != 'true'
|
|
run: |
|
|
echo "fuzzer build failed, see above for why"
|
|
echo "if the failure is due to OSS-Fuzz not being on the latest Go yet,"
|
|
echo "set TS_FUZZ_CURRENTLY_BROKEN=true in .github/workflows/test.yml"
|
|
echo "to temporarily disable fuzzing until OSS-Fuzz works again."
|
|
exit 1
|
|
- name: report unexpectedly working fuzz build
|
|
if: steps.build.outcome == 'success' && env.TS_FUZZ_CURRENTLY_BROKEN == 'true'
|
|
run: |
|
|
echo "fuzzer build succeeded, but we expect it to be broken"
|
|
echo "please set TS_FUZZ_CURRENTLY_BROKEN=false in .github/workflows/test.yml"
|
|
echo "to reenable fuzz testing"
|
|
exit 1
|
|
- name: run fuzzers
|
|
id: run
|
|
# Run the fuzzers whenever they're able to build, even if we're going to
|
|
# report a failure because TS_FUZZ_CURRENTLY_BROKEN is set to the wrong
|
|
# value.
|
|
if: steps.build.outcome == 'success'
|
|
# As of 12 February 2026, this repo doesn't tag releases, so this commit
|
|
# hash is just the tip of master.
|
|
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@f277aafb36f358582fdb24a41a9a52f2e097a2fd
|
|
with:
|
|
oss-fuzz-project-name: 'tailscale'
|
|
fuzz-seconds: 150
|
|
dry-run: false
|
|
language: go
|
|
- name: Set artifacts_path in env (workaround for actions/upload-artifact#176)
|
|
if: steps.run.outcome != 'success' && steps.build.outcome == 'success'
|
|
run: |
|
|
echo "artifacts_path=$(realpath .)" >> $GITHUB_ENV
|
|
- name: upload crash
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
if: steps.run.outcome != 'success' && steps.build.outcome == 'success'
|
|
with:
|
|
name: artifacts
|
|
path: ${{ env.artifacts_path }}/out/artifacts
|
|
|
|
depaware:
|
|
runs-on: ubuntu-24.04
|
|
needs: gomod-cache
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Set GOMODCACHE env
|
|
run: echo "GOMODCACHE=$HOME/.cache/go-mod" >> $GITHUB_ENV
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: check depaware
|
|
working-directory: src
|
|
run: make depaware
|
|
|
|
go_generate:
|
|
runs-on: ubuntu-24.04
|
|
needs: gomod-cache
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: check that 'go generate' is clean
|
|
working-directory: src
|
|
run: |
|
|
pkgs=$(./tool/go list ./... | grep -Ev 'dnsfallback|k8s-operator|xdp')
|
|
./tool/go generate $pkgs
|
|
git add -N . # ensure untracked files are noticed
|
|
echo
|
|
echo
|
|
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1)
|
|
- name: check that 'genreadme' is clean
|
|
working-directory: src
|
|
run: |
|
|
./tool/go run ./misc/genreadme
|
|
git add -N . # ensure untracked files are noticed
|
|
echo
|
|
echo
|
|
git diff --name-only --exit-code || (echo "The files above need updating. Please run './tool/go run ./misc/genreadme'."; exit 1)
|
|
|
|
make_tidy:
|
|
runs-on: ubuntu-24.04
|
|
needs: gomod-cache
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: check that 'make tidy' is clean
|
|
working-directory: src
|
|
run: |
|
|
make tidy
|
|
echo
|
|
echo
|
|
git diff --name-only --exit-code || (echo "Please run 'make tidy'"; exit 1)
|
|
|
|
licenses:
|
|
runs-on: ubuntu-24.04
|
|
needs: gomod-cache
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: check licenses
|
|
working-directory: src
|
|
run: |
|
|
grep -q TestLicenseHeaders *.go || (echo "Expected a test named TestLicenseHeaders"; exit 1)
|
|
./tool/go test -v -run=TestLicenseHeaders
|
|
|
|
staticcheck:
|
|
runs-on: ubuntu-24.04
|
|
needs: gomod-cache
|
|
name: staticcheck (${{ matrix.name }})
|
|
strategy:
|
|
fail-fast: false # don't abort the entire matrix if one element fails
|
|
matrix:
|
|
include:
|
|
- name: "macOS"
|
|
goos: "darwin"
|
|
goarch: "arm64"
|
|
flags: "--with-tags-all=darwin"
|
|
- name: "Windows"
|
|
goos: "windows"
|
|
goarch: "amd64"
|
|
flags: "--with-tags-all=windows"
|
|
- name: "Linux"
|
|
goos: "linux"
|
|
goarch: "amd64"
|
|
flags: "--with-tags-all=linux"
|
|
- name: "Portable (1/4)"
|
|
goos: "linux"
|
|
goarch: "amd64"
|
|
flags: "--without-tags-any=windows,darwin,linux --shard=1/4"
|
|
- name: "Portable (2/4)"
|
|
goos: "linux"
|
|
goarch: "amd64"
|
|
flags: "--without-tags-any=windows,darwin,linux --shard=2/4"
|
|
- name: "Portable (3/4)"
|
|
goos: "linux"
|
|
goarch: "amd64"
|
|
flags: "--without-tags-any=windows,darwin,linux --shard=3/4"
|
|
- name: "Portable (4/4)"
|
|
goos: "linux"
|
|
goarch: "amd64"
|
|
flags: "--without-tags-any=windows,darwin,linux --shard=4/4"
|
|
|
|
steps:
|
|
- name: checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: src
|
|
- name: Restore Go module cache
|
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
|
with:
|
|
path: gomodcache
|
|
key: ${{ needs.gomod-cache.outputs.cache-key }}
|
|
enableCrossOsArchive: true
|
|
- name: run staticcheck (${{ matrix.name }})
|
|
working-directory: src
|
|
run: |
|
|
export GOROOT=$(./tool/go env GOROOT)
|
|
./tool/go run -exec \
|
|
"env GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }}" \
|
|
honnef.co/go/tools/cmd/staticcheck -- \
|
|
$(./tool/go run ./tool/listpkgs --ignore-3p --goos=${{ matrix.goos }} --goarch=${{ matrix.goarch }} ${{ matrix.flags }} ./...)
|
|
|
|
notify_slack:
|
|
if: always()
|
|
# Any of these jobs failing causes a slack notification.
|
|
needs:
|
|
- android
|
|
- test
|
|
- windows
|
|
- macos
|
|
- cross
|
|
- ios
|
|
- wasm
|
|
- tailscale_go
|
|
- fuzz
|
|
- depaware
|
|
- go_generate
|
|
- make_tidy
|
|
- licenses
|
|
- staticcheck
|
|
runs-on: ubuntu-24.04
|
|
steps:
|
|
- name: notify
|
|
# Only notify slack for merged commits, not PR failures.
|
|
#
|
|
# It may be tempting to move this condition into the job's 'if' block, but
|
|
# don't: Github only collapses the test list into "everything is OK" if
|
|
# all jobs succeeded. A skipped job results in the list staying expanded.
|
|
# By having the job always run, but skipping its only step as needed, we
|
|
# let the CI output collapse nicely in PRs.
|
|
if: failure() && github.event_name == 'push'
|
|
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
|
|
with:
|
|
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
webhook-type: incoming-webhook
|
|
payload: |
|
|
{
|
|
"attachments": [{
|
|
"title": "Failure: ${{ github.workflow }}",
|
|
"title_link": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks",
|
|
"text": "${{ github.repository }}@${{ github.ref_name }}: <https://github.com/${{ github.repository }}/commit/${{ github.sha }}|${{ github.sha }}>",
|
|
"fields": [{ "value": ${{ toJson(github.event.head_commit.message) }}, "short": false }],
|
|
"footer": "${{ github.event.head_commit.committer.name }} at ${{ github.event.head_commit.timestamp }}",
|
|
"color": "danger"
|
|
}]
|
|
}
|
|
|
|
notify_windows:
|
|
if: always()
|
|
needs:
|
|
- windows
|
|
runs-on: ubuntu-24.04
|
|
steps:
|
|
- name: Download Windows integration results
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
pattern: windows-integration-results-*
|
|
path: results
|
|
- name: Build Slack payload
|
|
id: payload
|
|
env:
|
|
WINDOWS_RESULT: ${{ needs.windows.result }}
|
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
# head.ref is untrusted on fork PRs; keep it in env and encode via jq --arg, never run: text.
|
|
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
# Non-bench legs of the windows matrix; a missing artifact means a shard never reported.
|
|
EXPECTED_SHARDS: "2"
|
|
run: |
|
|
# Read each artifact separately so one unreadable file can't discard the others.
|
|
rows='[]'; unreadable=0; reported=0
|
|
while IFS= read -r -d '' f; do
|
|
reported=$((reported + 1))
|
|
[ -s "$f" ] || continue
|
|
case "$(jq -r 'type' "$f" 2>/dev/null)" in
|
|
array) rows=$(printf '%s\n%s' "$rows" "$(jq -c . "$f")" | jq -s 'add') ;;
|
|
null) ;; # shard ran none of the filtered package
|
|
*) unreadable=$((unreadable + 1)) ;;
|
|
esac
|
|
done < <(find results -name windows-integration-results.json -print0 2>/dev/null)
|
|
# Dedupe so a re-shard reporting a test twice can't double every count.
|
|
rows=$(printf '%s' "$rows" | jq '[ .[] | objects ] | unique_by([.Package, .Test])')
|
|
count() { printf '%s' "$rows" | jq "[ (. // [])[] | select(.Outcome == \"$1\") ] | length"; }
|
|
retried=$(count retried)
|
|
passed=$(( $(count pass) + retried ))
|
|
failed=$(count fail)
|
|
skipped=$(count skip)
|
|
executed=$((passed + failed))
|
|
if [ "$executed" -gt 0 ]; then pct=$(( (passed * 100) / executed )); else pct=0; fi
|
|
# Names behind the retried count, capped so one bad run can't flood the message.
|
|
flaky=$(printf '%s' "$rows" | jq -r '
|
|
[ .[] | select(.Outcome == "retried") | (.Package | sub("^tailscale\\.com/"; "")) + "." + .Test ]
|
|
| sort
|
|
| if length > 5 then (.[:5] | join(", ")) + ", and \(length - 5) more" else join(", ") end')
|
|
|
|
case "$WINDOWS_RESULT" in
|
|
success) emoji=":white_check_mark:"; color="good" ;;
|
|
failure) emoji=":x:"; color="danger" ;;
|
|
cancelled) emoji=":no_entry_sign:"; color="warning" ;;
|
|
skipped) emoji=":fast_forward:"; color="warning" ;;
|
|
*) emoji=":question:"; color="warning" ;;
|
|
esac
|
|
|
|
integration="${passed} passed, ${failed} failed, ${skipped} skipped"
|
|
if [ "$executed" -gt 0 ]; then
|
|
integration="${integration} (${pct}%)"
|
|
fi
|
|
# Anything that makes the numbers incomplete or overstated is said out loud.
|
|
caveats=""
|
|
if [ "$unreadable" -gt 0 ]; then
|
|
caveats="${caveats} — ${unreadable} shard artifact(s) unreadable"
|
|
fi
|
|
if [ "$reported" -lt "$EXPECTED_SHARDS" ]; then
|
|
caveats="${caveats} — only ${reported} of ${EXPECTED_SHARDS} shards reported"
|
|
fi
|
|
# Retries get their own line below, so they don't warn the count line.
|
|
if [ "$executed" -eq 0 ]; then
|
|
caveats="${caveats} — no tests executed"
|
|
fi
|
|
if [ "$WINDOWS_RESULT" = "failure" ] && [ "$failed" -eq 0 ]; then
|
|
caveats="${caveats} — results may be partial (build error or timeout)"
|
|
fi
|
|
integration="${integration}${caveats}"
|
|
if [ "$failed" -gt 0 ]; then int_emoji=":x:"
|
|
elif [ -n "$caveats" ]; then int_emoji=":warning:"
|
|
else int_emoji=":white_check_mark:"; fi
|
|
|
|
jq -n --arg emoji "$emoji" --arg status "$WINDOWS_RESULT" --arg int_emoji "$int_emoji" \
|
|
--arg integration "$integration" --arg flaky "$flaky" --arg retried "$retried" \
|
|
--arg branch "$HEAD_REF" --arg title "$PR_TITLE" --arg pr_url "$PR_URL" --arg pr_number "$PR_NUMBER" \
|
|
--arg color "$color" --arg run_url "$RUN_URL" \
|
|
'{attachments: [{
|
|
title: ($emoji + " " + $title + ": " + $status),
|
|
title_link: $run_url,
|
|
text: (
|
|
"<" + $pr_url + "|PR #" + $pr_number + "> · `" + $branch + "`"
|
|
+ "\n\n" + $emoji + " Windows CI: " + $status
|
|
+ "\n" + $int_emoji + " Windows Integration Tests: " + $integration
|
|
+ (if $flaky == "" then "" else "\n ↳ :warning: Flaky Tests (" + $retried + " passed on retry): " + $flaky end)
|
|
+ "\n\n<" + $run_url + "|View run details>"
|
|
),
|
|
color: $color,
|
|
mrkdwn_in: ["text"]
|
|
}]}' > payload.json
|
|
- name: Send Slack notification
|
|
env:
|
|
WEBHOOK: ${{ secrets.WINDOWS_SLACK_WEBHOOK_URL }}
|
|
if: env.WEBHOOK != '' && github.event_name == 'pull_request' && needs.windows.result != 'cancelled'
|
|
continue-on-error: true
|
|
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
|
|
with:
|
|
webhook: ${{ env.WEBHOOK }}
|
|
webhook-type: incoming-webhook
|
|
payload-file-path: payload.json
|
|
|
|
merge_blocker:
|
|
if: always()
|
|
runs-on: ubuntu-24.04
|
|
needs:
|
|
- android
|
|
- test
|
|
- windows
|
|
- macos
|
|
- cross
|
|
- ios
|
|
- wasm
|
|
- tailscale_go
|
|
- fuzz
|
|
- depaware
|
|
- go_generate
|
|
- make_tidy
|
|
- licenses
|
|
- staticcheck
|
|
steps:
|
|
- name: Decide if change is okay to merge
|
|
if: github.event_name != 'push'
|
|
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2
|
|
with:
|
|
jobs: ${{ toJSON(needs) }}
|
|
|
|
# This waits on all the jobs which must never fail. Branch protection rules
|
|
# enforce these. No flaky tests are allowed in these jobs. (We don't want flaky
|
|
# tests anywhere, really, but a flaky test here prevents merging.)
|
|
check_mergeability_strict:
|
|
if: always()
|
|
runs-on: ubuntu-24.04
|
|
needs:
|
|
- android
|
|
- cross
|
|
- crossmin
|
|
- ios
|
|
- tailscale_go
|
|
- depaware
|
|
- go_generate
|
|
- make_tidy
|
|
- licenses
|
|
- staticcheck
|
|
steps:
|
|
- name: Decide if change is okay to merge
|
|
if: github.event_name != 'push'
|
|
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2
|
|
with:
|
|
jobs: ${{ toJSON(needs) }}
|
|
|
|
check_mergeability:
|
|
if: always()
|
|
runs-on: ubuntu-24.04
|
|
needs:
|
|
- check_mergeability_strict
|
|
- test
|
|
- windows
|
|
- macos
|
|
- wasm
|
|
- fuzz
|
|
- race-root-integration
|
|
- privileged
|
|
steps:
|
|
- name: Decide if change is okay to merge
|
|
if: github.event_name != 'push'
|
|
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2
|
|
with:
|
|
jobs: ${{ toJSON(needs) }}
|