tailscale/.github/workflows/ssh-integrationtest.yml
Kristoffer Dalby 69c2fa774b .github: add macOS SSH integration test job with diagnostic capture
Add a macos-ssh-integrationtest job that exercises the SSH exit-status
behavior on a real macos-latest runner using the existing 'runner'
account, complementing the existing Docker-container matrix on Linux.

The job runs the two macOS-relevant subtests of the integration test
binary (TestOpenSSHExitCodes, TestIntegrationExitCodes) under sudo so
the incubator's drop-privileges path is exercised, with the test user
plumbed through TS_SSH_INTEGRATION_TEST_USER=runner.

Diagnostic capture is the load-bearing part of this job:

  A preflight step dumps uname, sw_vers, id, id runner, ssh -V, the
  shell registered in Open Directory for the runner account, the
  /etc/ssh listing, and sudo capability. This captures every piece
  of context a future failure on this runner will need, before the
  test binary even runs.

  PATH propagation through sudo is an explicit, expanded list
  (/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH) instead of
  inheriting the GitHub Actions PATH, which doesn't always include
  /usr/sbin where macOS keeps utilities the incubator may invoke.

  Both test steps use continue-on-error: true with id-tagged
  outcomes; a final aggregator step fails the job iff either step
  failed. This guarantees that a failure in one test still lets the
  other run, doubling the signal we get from each failed CI run.

  Test output is tee'd to per-step log files, and /tmp/tailscalessh.log
  (the incubator log, owned by root after sudo) is sudo-copied and
  made readable. All three artifacts are uploaded with 14-day
  retention via actions/upload-artifact, pinned to v7.0.0 to match
  the rest of the repo.

The per-step timeout is 5m, giving the in-test 3-attempt retry logic
headroom without hitting the workflow timeout before the final
attempt.

Updates #18256

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2026-05-06 09:38:56 +00:00

142 lines
5.2 KiB
YAML

# Run the ssh integration tests in various Docker containers.
# These tests can also be run locally via `make sshintegrationtest`.
name: "ssh-integrationtest"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
pull_request:
paths:
- "ssh/**"
- "tempfork/gliderlabs/ssh/**"
- ".github/workflows/ssh-integrationtest.yml"
jobs:
ssh-integrationtest:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- base: "ubuntu:focal"
tag: "ssh-ubuntu-focal"
- base: "ubuntu:jammy"
tag: "ssh-ubuntu-jammy"
- base: "ubuntu:noble"
tag: "ssh-ubuntu-noble"
- base: "alpine:latest"
tag: "ssh-alpine-latest"
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Build test binaries
run: |
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 ./tool/go test -tags integrationtest -c ./ssh/tailssh -o ssh/tailssh/testcontainers/tailssh.test
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 ./tool/go build -o ssh/tailssh/testcontainers/tailscaled ./cmd/tailscaled
- name: Run SSH integration tests (${{ matrix.base }})
run: |
docker build --build-arg="BASE=${{ matrix.base }}" -t "${{ matrix.tag }}" ssh/tailssh/testcontainers
macos-ssh-integrationtest:
runs-on: macos-latest
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Capture environment context up front so future failures can be
# diagnosed from the artifact alone, even if the test binary fails
# before producing useful output of its own.
- name: macOS preflight diagnostics
run: |
set +e
echo "=== uname -a ==="
uname -a
echo "=== sw_vers ==="
sw_vers
echo "=== id ==="
id
echo "=== id runner ==="
id runner
echo "=== ssh -V ==="
ssh -V
echo "=== which ssh ==="
which ssh
echo "=== dscl . -read /Users/runner UserShell ==="
dscl . -read /Users/runner UserShell
echo "=== /etc/ssh listing ==="
ls -la /etc/ssh/ || true
echo "=== sudo -n true ==="
sudo -n true && echo "passwordless sudo OK" || echo "passwordless sudo NOT available"
echo "=== PATH ==="
echo "$PATH"
- name: Build test binaries
run: |
./tool/go test -tags integrationtest -c ./ssh/tailssh -o /tmp/tailssh.test
./tool/go build -o /tmp/tailscaled ./cmd/tailscaled
ls -l /tmp/tailssh.test /tmp/tailscaled
# The two test steps below are wired with continue-on-error so a
# failure in one still lets the other run. The aggregator step at
# the end fails the job if either failed. We need both: the Go SSH
# client test exercises the wire-level frame ordering, and the
# OpenSSH client test exercises the actual binary that users of
# #18256 are running.
- name: Run macOS OpenSSH exit status integration tests
id: openssh
continue-on-error: true
run: |
set -o pipefail
sudo env \
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH \
TAILSCALED_PATH=/tmp/tailscaled \
TS_SSH_INTEGRATION_TEST_USER=runner \
/tmp/tailssh.test -test.v -test.timeout=5m -test.run '^TestOpenSSHExitCodes$' \
2>&1 | tee /tmp/openssh-exitcodes.log
- name: Run macOS Go SSH exit status integration tests
id: gossh
continue-on-error: true
run: |
set -o pipefail
sudo env \
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH \
TAILSCALED_PATH=/tmp/tailscaled \
TS_SSH_INTEGRATION_TEST_USER=runner \
/tmp/tailssh.test -test.v -test.timeout=5m -test.run '^TestIntegrationExitCodes$' \
2>&1 | tee /tmp/gossh-exitcodes.log
- name: Collect incubator log
if: always()
run: |
if [ -f /tmp/tailscalessh.log ]; then
sudo cp /tmp/tailscalessh.log /tmp/tailscalessh.log.copy || true
sudo chmod a+r /tmp/tailscalessh.log.copy || true
else
echo "no incubator log produced" > /tmp/tailscalessh.log.copy
fi
- name: Upload diagnostic artifacts
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: macos-ssh-integrationtest-logs
path: |
/tmp/openssh-exitcodes.log
/tmp/gossh-exitcodes.log
/tmp/tailscalessh.log.copy
if-no-files-found: warn
retention-days: 14
- name: Aggregate test result
run: |
openssh="${{ steps.openssh.outcome }}"
gossh="${{ steps.gossh.outcome }}"
echo "OpenSSH test outcome: $openssh"
echo "Go SSH test outcome: $gossh"
if [ "$openssh" != "success" ] || [ "$gossh" != "success" ]; then
exit 1
fi