Update integration testing for agent enrollments (#41981)

Add nightly testing across the following:

OS: mac/Linux/Windows
Updates: enabled/disabled
Channels (for each of orbit/osquery\desktop): edge/stable
Arch: arm/x86

Failures are alerted to Slack.
This commit is contained in:
Zach Wasserman
2026-03-25 22:29:47 -07:00
committed by GitHub
parent 0cdad44c2c
commit 1a82975b37
2 changed files with 583 additions and 483 deletions
+583
View File
@@ -0,0 +1,583 @@
# This workflow tests enrolling of agents on the supported platforms.
#
# It starts the latest release of fleet with the "fleetctl preview" command.
# It generates the installers for the latest version of fleetd with the
# "fleetctl package" command.
#
# It tests across a matrix of configurations:
# OS: mac/Linux/Windows
# Updates: enabled/disabled
# Channels (for each of orbit/osquery\desktop): edge/stable
# Arch: arm/x86
#
# Troubleshooting
# The top two errors seen while developing this:
# 1) Jobs are queued waiting for runners long enough for the entire workflow to fail. Scheduling for the middle of the night attempts to mitigate this. Timeouts have been tuned to try to manage it as well.
# 2) Network issues (commonly related to Cloudflare tunnels) cause some request to fail.
#
# Upon failure, the workflow will automatically retry up to 3 times. Notifications are sent to Slack upon failure, and also after the failure has been resolved. After 4 failures, a stronger message will be logged to Slack.
name: E2E Test Agents
on:
workflow_dispatch: # Manual
inputs:
retry:
description: 'Number of retries attempted so far'
type: number
default: 0
schedule:
- cron: '0 5 * * *' # Nightly 5AM UTC
pull_request:
paths:
- '.github/workflows/e2e-agent.yml'
# Each cron schedule gets its own concurrency group. workflow_dispatch and pull_request also get their own.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}-${{ github.event.schedule || github.event_name }}
cancel-in-progress: true
defaults:
run:
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
shell: bash
jobs:
# Generate a random UUID to be used for the Cloudflare tunnel subdomain and make it available to later jobs.
gen:
runs-on: ubuntu-latest
outputs:
subdomain: ${{ steps.gen.outputs.subdomain }}
address: ${{ steps.gen.outputs.address }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- id: gen
run: |
UUID=$(uuidgen)
echo "subdomain=fleet-test-$UUID" >> $GITHUB_OUTPUT
echo "address=https://fleet-test-$UUID.fleetuem.com" >> $GITHUB_OUTPUT
run-server:
timeout-minutes: 240
runs-on: ubuntu-latest
needs: gen
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Start tunnel
env:
CERT_PEM: ${{ secrets.CLOUDFLARE_TUNNEL_FLEETUEM_CERT_B64 }}
run: |
# Increase maximum receive buffer size to roughly 2.5 MB.
# Cloudflared uses quic-go. This buffer holds packets that have been received by the kernel,
# but not yet read by the application (quic-go in this case). Once this buffer fills up, the
# kernel will drop any new incoming packet.
# See https://github.com/quic-go/quic-go/wiki/UDP-Receive-Buffer-Size.
sudo sysctl -w net.core.rmem_max=2500000
# Install cloudflared and run tunnel
wget https://github.com/cloudflare/cloudflared/releases/download/2026.3.0/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
echo "$CERT_PEM" | base64 -d > cert.pem
cloudflared tunnel --origincert cert.pem create ${{ needs.gen.outputs.subdomain }}
cloudflared tunnel --origincert cert.pem route dns ${{ needs.gen.outputs.subdomain }} ${{ needs.gen.outputs.subdomain }}
cloudflared tunnel --origincert cert.pem --url http://localhost:1337 --logfile cloudflared.log run ${{ needs.gen.outputs.subdomain }} &
until [[ $(cloudflared tunnel --origincert cert.pem info -o json ${{ needs.gen.outputs.subdomain }} | jq '.conns[0].conns[0].is_pending_reconnect') = false ]]; do
echo "Awaiting tunnel ready..."
sleep 1
done
- name: Run Fleet server
run: |
npm install -g fleetctl
fleetctl preview --no-hosts --disable-open-browser
fleetctl config set --address ${{ needs.gen.outputs.address }}
fleetctl get enroll-secret
docker compose -f ~/.fleet/preview/docker-compose.yml logs --follow fleet01 fleet02 &
# Ensure Fleet server is responding before waiting for enrollments
echo "Checking Fleet server health..."
HEALTH_CHECK_COUNT=0
until HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" http://localhost:1337/healthz) && [[ "$HTTP_CODE" == "200" ]]; do
HEALTH_CHECK_COUNT=$((HEALTH_CHECK_COUNT + 1))
if [ $HEALTH_CHECK_COUNT -ge 30 ]; then
echo "ERROR: Fleet server not responding after 150 seconds"
docker ps -a --filter "name=fleet"
exit 1
fi
echo "Health check ${HEALTH_CHECK_COUNT}/30 (HTTP status: ${HTTP_CODE:-connection failed})"
sleep 5
done
echo "Fleet server is responding"
# Wait for all hosts to enroll, then keep the server alive until the summary job completes.
EXPECTED=96 # This needs to be updated when the matrix strategies are updated.
START=$(date +%s)
while true; do
ELAPSED=$(( $(date +%s) - START ))
# Check and display enrollment status
fleetctl get hosts || true
HOST_COUNT=$(fleetctl get hosts --json | (grep -v "No hosts found" || true) | wc -l | tr -d ' ')
echo "Hosts enrolled: ${HOST_COUNT} / $EXPECTED (${ELAPSED}s)"
# Check summary job status
JOBS_JSON=$(gh api "/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs?per_page=100")
SUMMARY_STATUS=$(echo "$JOBS_JSON" | jq -r '[.jobs[] | select(.name == "summary")] | if length > 0 then .[0].status else "not_started" end')
echo "Summary job status: $SUMMARY_STATUS"
if [ "$SUMMARY_STATUS" = "completed" ]; then
echo "Summary job completed, exiting."
break
fi
sleep 10
done
env:
GH_TOKEN: ${{ github.token }}
- name: Show enrolled hosts
if: always()
run: |
fleetctl get hosts
fleetctl get hosts --json | jq
- name: Cleanup tunnel
if: always()
run: cloudflared tunnel --origincert cert.pem delete --force ${{ needs.gen.outputs.subdomain }} || true
- name: Print cloudflared logs
if: always()
run: cat cloudflared.log || true
- name: Cancel workflow if run-server fails
if: failure()
run: gh run cancel ${{ github.run_id }} --repo fleetdm/fleet
env:
GH_TOKEN: ${{ secrets.FLEET_RELEASE_GITHUB_PAT }}
login:
timeout-minutes: 15
runs-on: ubuntu-latest
needs: gen
outputs:
token: ${{ steps.login.outputs.token }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
# Login only here and share the token because otherwise we could hit rate limits.
- name: Set Cloudflare DNS
run: |
# Use Cloudflare's DNS resolver (1.1.1.1) since the tunnel DNS record is managed
# by Cloudflare. Apply to all non-loopback interfaces in case traffic routes
# through one other than eth0.
for iface in $(ip -o link show | awk -F': ' '{print $2}' | grep -v lo); do
sudo resolvectl dns "$iface" 1.1.1.1 2>/dev/null || true
done
- id: login
name: Attempt login
run: |
npm install -g fleetctl
fleetctl config set --address ${{ needs.gen.outputs.address }}
# Wait for DNS to propagate by querying Cloudflare's DoH endpoint over HTTPS.
# This avoids relying on UDP/53 to 1.1.1.1, which may be blocked on runners.
HOSTNAME=$(echo "${{ needs.gen.outputs.address }}" | sed 's|https://||')
echo "Waiting for DNS propagation..."
DNS_START=$(date +%s)
until curl -sf "https://1.1.1.1/dns-query?name=${HOSTNAME}&type=A" \
-H 'accept: application/dns-json' | jq -e '.Status == 0' > /dev/null; do
ELAPSED=$(( $(date +%s) - DNS_START ))
echo "DNS not yet propagated... (${ELAPSED}s)"
sleep 2
done
echo "DNS propagated."
# Wait for Fleet server to be reachable
echo "Waiting for Fleet server to pass health check..."
HEALTH_CHECK_START=$(date +%s)
until curl -s -o /dev/null -w "%{http_code}" ${{ needs.gen.outputs.address }}/healthz | grep -q "200"; do
ELAPSED=$(( $(date +%s) - HEALTH_CHECK_START ))
echo "Health check failed... (${ELAPSED}s)"
sleep 1
done
echo "Fleet server is responding, attempting login..."
LOGIN_START=$(date +%s)
until fleetctl login --email admin@example.com --password preview1337#; do
ELAPSED=$(( $(date +%s) - LOGIN_START ))
echo "Login attempt failed... (${ELAPSED}s)"
sleep 1
done
TOKEN=$(fleetctl config get token | awk '{print $3}')
echo "token=$TOKEN" >> $GITHUB_OUTPUT
fleetd-macos:
timeout-minutes: 10
strategy:
matrix:
runner: [ 'macos-15', 'macos-15-intel' ]
orbit-channel: [ 'stable', 'edge' ]
osqueryd-channel: [ 'stable', 'edge' ]
desktop-channel: [ 'stable', 'edge' ]
disable-updates: [ true, false ]
runs-on: ${{ matrix.runner }}
needs: [gen, login]
steps:
- name: Install fleetctl
run: |
npm install -g fleetctl
fleetctl config set --address ${{ needs.gen.outputs.address }} --token ${{ needs.login.outputs.token }}
- name: Set Cloudflare DNS
run: |
# Use Cloudflare's DNS resolver (1.1.1.1) since the tunnel DNS record is managed
# by Cloudflare — their resolver sees the new record immediately.
for svc in $(networksetup -listallnetworkservices | tail -n +2); do
sudo networksetup -setdnsservers "$svc" 1.1.1.1 2>/dev/null || true
done
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder || true
- name: Install fleetd
run: |
ARCH=$(uname -m)
sudo hostname macos-${ARCH}-${{ matrix.orbit-channel }}-${{ matrix.osqueryd-channel }}-${{ matrix.desktop-channel }}-${{ matrix.disable-updates }}
SECRET_JSON=$(fleetctl get enroll_secret --json --debug)
echo $SECRET_JSON
SECRET=$(echo $SECRET_JSON | jq -r '.spec.secrets[0].secret')
echo "Secret: $SECRET"
echo "Hostname: $(hostname -s)"
# Instance identifier is needed because macOS runners share UUIDs
fleetctl package --type pkg --fleet-url=${{ needs.gen.outputs.address }} --enroll-secret=$SECRET --orbit-channel=${{ matrix.orbit-channel }} --osqueryd-channel=${{ matrix.osqueryd-channel }} --desktop-channel=${{ matrix.desktop-channel }} --fleet-desktop --debug --host-identifier=instance --disable-updates=${{ matrix.disable-updates }}
sudo installer -pkg fleet-osquery.pkg -target /
ENROLLMENT_START=$(date +%s)
until fleetctl get hosts | grep -iF $(hostname -s);
do
CURRENT_TIME=$(date +%s)
ELAPSED=$((CURRENT_TIME - ENROLLMENT_START))
echo "Awaiting enrollment... (${ELAPSED}s)"
sleep 1
done
- name: Check processes
run: |
sleep 30
sudo tail -60 /var/log/orbit/orbit.stderr.log
echo "Checking if osqueryd is running..."
pgrep -x osqueryd || (echo "ERROR: osqueryd is not running" && exit 1)
echo "Checking if orbit is running..."
pgrep -x orbit || (echo "ERROR: orbit is not running" && exit 1)
echo "Checking if fleet-desktop is running..."
pgrep -x fleet-desktop || (echo "ERROR: fleet-desktop is not running" && exit 1)
echo "All processes are running."
- name: Print orbit logs
if: always()
run: |
sudo cat /var/log/orbit/*
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
sparse-checkout: |
it-and-security/lib/macos/scripts/uninstall-fleetd-macos.sh
- name: Uninstall Orbit
run: |
sudo ./it-and-security/lib/macos/scripts/uninstall-fleetd-macos.sh
fleetd-ubuntu:
timeout-minutes: 10
strategy:
matrix:
runner: [ 'ubuntu-24.04', 'ubuntu-24.04-arm' ]
orbit-channel: [ 'stable', 'edge' ]
osqueryd-channel: [ 'stable', 'edge' ]
desktop-channel: [ 'stable', 'edge' ]
disable-updates: [ true, false ]
runs-on: ${{ matrix.runner }}
needs: [gen, login]
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Install fleetctl
run: |
npm install -g fleetctl
fleetctl config set --address ${{ needs.gen.outputs.address }} --token ${{ needs.login.outputs.token }}
- name: Set Cloudflare DNS
run: |
# Use Cloudflare's DNS resolver (1.1.1.1) since the tunnel DNS record is managed
# by Cloudflare. Apply to all non-loopback interfaces in case traffic routes
# through one other than eth0.
for iface in $(ip -o link show | awk -F': ' '{print $2}' | grep -v lo); do
sudo resolvectl dns "$iface" 1.1.1.1 2>/dev/null || true
done
- name: Install Orbit
run: |
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then FLEET_ARCH="amd64"; else FLEET_ARCH="arm64"; fi
sudo hostnamectl set-hostname ubuntu-${ARCH}-${{ matrix.orbit-channel }}-${{ matrix.osqueryd-channel }}-${{ matrix.desktop-channel }}-${{ matrix.disable-updates }}
SECRET_JSON=$(fleetctl get enroll_secret --json --debug)
echo $SECRET_JSON
SECRET=$(echo $SECRET_JSON | jq -r '.spec.secrets[0].secret')
echo "Secret: $SECRET"
echo "Hostname: $(hostname -s)"
fleetctl package --type deb --fleet-url=${{ needs.gen.outputs.address }} --enroll-secret=$SECRET --orbit-channel=${{ matrix.orbit-channel }} --osqueryd-channel=${{ matrix.osqueryd-channel }} --desktop-channel=${{ matrix.desktop-channel }} --fleet-desktop --debug --arch=$FLEET_ARCH --disable-updates=${{ matrix.disable-updates }}
sudo dpkg -i fleet-osquery*
ENROLLMENT_START=$(date +%s)
until fleetctl get hosts | grep -iF $(hostname -s); do
CURRENT_TIME=$(date +%s)
ELAPSED=$((CURRENT_TIME - ENROLLMENT_START))
echo "Waiting for enrollment... (${ELAPSED}s)"
sudo systemctl status orbit.service || true
sleep 1
done
- name: Check processes
run: |
sudo systemctl status orbit.service
sleep 30
sudo systemctl status orbit.service
echo "Checking if osqueryd is running..."
pgrep -x osqueryd || (echo "ERROR: osqueryd is not running" && exit 1)
echo "Checking if orbit is running..."
pgrep -x orbit || (echo "ERROR: orbit is not running" && exit 1)
# Don't check for Fleet Desktop as it doesn't run in the windowless CI environment.
echo "All processes are running."
- name: Print orbit logs
if: always()
run: |
sudo journalctl -u orbit.service --no-pager
- name: Uninstall Orbit
run: |
sudo apt remove fleet-osquery -y
fleetd-windows:
timeout-minutes: 10
strategy:
matrix:
runner: [ 'windows-2025', 'windows-11-arm' ]
orbit-channel: [ 'stable', 'edge' ]
osqueryd-channel: [ 'stable', 'edge' ]
desktop-channel: [ 'stable', 'edge' ]
disable-updates: [ true, false ]
needs: [gen, login]
runs-on: ${{ matrix.runner }}
steps:
# We need to use some shenanigans to rename the Windows computer without restarting. Note: Windows computers should not get names longer than 15 characters (confirmed this breaks networking).
- name: Rename computer
shell: powershell
run: |
$orbit = "${{ matrix.orbit-channel }}"
$osqueryd = "${{ matrix.osqueryd-channel }}"
$desktop = "${{ matrix.desktop-channel }}"
$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'a' } else { 'x' }
$disableUpdates = if ("${{ matrix.disable-updates }}" -eq "true") { "t" } else { "f" }
$ComputerName = "win-$arch-$($orbit[0])-$($osqueryd[0])-$($desktop[0])-$disableUpdates"
echo "Setting computer name to $ComputerName"
Remove-ItemProperty -path "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" -name "Hostname"
Remove-ItemProperty -path "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" -name "NV Hostname"
Set-ItemProperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\Computername\Computername" -name "Computername" -value $ComputerName
Set-ItemProperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\Computername\ActiveComputername" -name "Computername" -value $ComputerName
Set-ItemProperty -path "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" -name "Hostname" -value $ComputerName
Set-ItemProperty -path "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" -name "NV Hostname" -value $ComputerName
Set-ItemProperty -path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -name "AltDefaultDomainName" -value $ComputerName
Set-ItemProperty -path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -name "DefaultDomainName" -value $ComputerName
- name: Set Cloudflare DNS
shell: powershell
run: |
# Use Cloudflare's DNS resolver (1.1.1.1) since the tunnel DNS record is managed
# by Cloudflare — their resolver sees the new record immediately.
# -ErrorAction SilentlyContinue skips adapters (e.g. Hyper-V virtual/internal)
# that have no associated DNS client address object.
Get-NetAdapter | ForEach-Object { Set-DnsClientServerAddress -InterfaceIndex $_.InterfaceIndex -ServerAddresses "1.1.1.1" -ErrorAction SilentlyContinue }
Clear-DnsClientCache
- name: Install fleetctl
shell: bash
# On Windows we need to set rootca or tls-skip verify. Since this is a test environment we can skip TLS verification.
run: |
npm install -g fleetctl
fleetctl config set --address ${{ needs.gen.outputs.address }} --token ${{ needs.login.outputs.token }} --tls-skip-verify
- name: Install WiX toolset (arm runner only)
if: matrix.runner == 'windows-11-arm'
shell: powershell
run: |
Invoke-WebRequest -Uri "https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe" -OutFile wix314.exe
Start-Process -Wait -FilePath .\wix314.exe -ArgumentList "/quiet"
"WIX=C:\Program Files (x86)\WiX Toolset v3.14\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Build MSI
shell: bash
run: |
SECRET_JSON=$(fleetctl get enroll_secret --json --debug)
echo "$SECRET_JSON"
# Strip any prefix before the JSON (e.g. "Installing fleetctl... Install completed. ") in case
# fleetctl auto-updates and writes an install message to stdout on the same line as the JSON.
SECRET=$(echo "$SECRET_JSON" | sed 's/^[^{]*//' | jq -r '.spec.secrets[0].secret')
echo "Secret: $SECRET"
ARCH=$(echo "$PROCESSOR_ARCHITECTURE" | tr '[:upper:]' '[:lower:]')
# WIX env var points to the WiX Toolset install dir (pre-installed on windows-11 runner, installed above on windows-11-arm)
fleetctl package --type msi --fleet-url=${{ needs.gen.outputs.address }} --enroll-secret=$SECRET --orbit-channel=${{ matrix.orbit-channel }} --osqueryd-channel=${{ matrix.osqueryd-channel }} --desktop-channel=${{ matrix.desktop-channel }} --fleet-desktop --debug --local-wix-dir="${WIX}bin" --arch=$ARCH --disable-updates=${{ matrix.disable-updates }}
- name: Install Orbit
shell: cmd
run: |
msiexec /i fleet-osquery.msi /quiet /passive /lv log.txt
- name: Wait for enrollment
shell: powershell
run: |
$orbit = "${{ matrix.orbit-channel }}"
$osqueryd = "${{ matrix.osqueryd-channel }}"
$desktop = "${{ matrix.desktop-channel }}"
$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'a' } else { 'x' }
$disableUpdates = if ("${{ matrix.disable-updates }}" -eq "true") { "t" } else { "f" }
$ComputerName = "win-$arch-$($orbit[0])-$($osqueryd[0])-$($desktop[0])-$disableUpdates"
$StartTime = Get-Date
do {
$hosts = fleetctl get hosts
if ($hosts -match $ComputerName) {
Write-Host "Success! $ComputerName enrolled."
break
}
$Elapsed = [math]::Round(((Get-Date) - $StartTime).TotalSeconds)
Write-Host "Waiting for enrollment... (${Elapsed}s)"
Start-Sleep -Seconds 1
} while ($true)
- name: Check processes
shell: powershell
run: |
Start-Sleep -Seconds 30
Write-Host "Checking if osqueryd is running..."
if (-not (Get-Process -Name "osqueryd" -ErrorAction SilentlyContinue)) {
Write-Host "ERROR: osqueryd is not running"
exit 1
}
Write-Host "Checking if orbit is running..."
if (-not (Get-Process -Name "orbit" -ErrorAction SilentlyContinue)) {
Write-Host "ERROR: orbit is not running"
exit 1
}
Write-Host "Checking if fleet-desktop is running..."
if (-not (Get-Process -Name "fleet-desktop" -ErrorAction SilentlyContinue)) {
Write-Host "ERROR: fleet-desktop is not running"
exit 1
}
Write-Host "All processes are running."
- name: Print orbit install log
if: always()
shell: powershell
run: Get-Content log.txt -ErrorAction SilentlyContinue
- name: Print Orbit logs
if: always()
shell: powershell
run: Get-Content "C:\Windows\system32\config\systemprofile\AppData\Local\FleetDM\Orbit\Logs\orbit-osquery.log" -ErrorAction SilentlyContinue
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
sparse-checkout: |
it-and-security/lib/windows/scripts/uninstall-fleetd-windows.ps1
- name: Uninstall Orbit
shell: powershell
run: |
.\it-and-security\lib\windows\scripts\uninstall-fleetd-windows.ps1
summary:
needs: [fleetd-macos, fleetd-ubuntu, fleetd-windows]
runs-on: ubuntu-latest
if: always()
steps:
- name: Compute next retry
id: next-retry
run: echo "value=$(( ${{ inputs.retry || 0 }} + 1 ))" >> $GITHUB_OUTPUT
- name: Slack Notification (failure with retries remaining)
if: (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && (inputs.retry || 0) > 0)) && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) && (inputs.retry || 0) < 3
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
webhook: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Agent E2E test FAILED* (attempt ${{ steps.next-retry.outputs.value }}/4, retrying...)\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View workflow run>\nThis may not need investigation if it self-resolves on the retry. Look for the next notification of success/failure."
- name: Slack Notification (all retries exhausted)
if: (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) && (inputs.retry || 0) >= 3
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
webhook: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
blocks:
- type: "header"
text:
type: "plain_text"
text: "🚨 ALL RETRIES EXHAUSTED — MANUAL INVESTIGATION REQUIRED 🚨"
- type: "section"
text:
type: "mrkdwn"
text: "*Agent E2E test FAILED all 4 attempts* :rotating_light:\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View workflow run>"
- name: Slack Notification (retry success)
if: ${{!contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && (inputs.retry || 0) > 0 }}
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
webhook: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Agent E2E test PASSED after ${{ inputs.retry }} retr${{ inputs.retry == 1 && 'y' || 'ies' }}* :white_check_mark:\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View workflow run>\nThe above failure appears to have been transient. No investigation needed unless you see a pattern of repeated failures."
- name: Retry workflow on failure
# Only retry scheduled runs or manual runs that are retries for scheduled runs (inputs.retry > 0)
if: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && (inputs.retry || 0) > 0)) && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) && (inputs.retry || 0) < 3 }}
run: |
gh workflow run e2e-agent.yml --repo ${{ github.repository }} --ref ${{ github.head_ref || github.ref_name }} -f retry=${{ steps.next-retry.outputs.value }}
env:
GH_TOKEN: ${{ secrets.FLEET_RELEASE_GITHUB_PAT }}
- name: Cancel workflow if any job failed
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: gh run cancel ${{ github.run_id }} --repo fleetdm/fleet
env:
GH_TOKEN: ${{ secrets.FLEET_RELEASE_GITHUB_PAT }}
-483
View File
@@ -1,483 +0,0 @@
# This workflow tests enrolling of agents on the supported platforms,
# using the latest version of fleet, fleetctl and orbit.
#
# It starts the latest release of fleet with the "fleetctl preview" command.
# It generates the installers for the latest version of Orbit with the
# "fleetctl package" command.
name: Test Fleetctl, Orbit & Preview
on:
workflow_dispatch: # Manual
schedule:
- cron: '0 2 * * *' # Nightly 2AM UTC
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
cancel-in-progress: true
defaults:
run:
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
shell: bash
permissions:
contents: read
jobs:
gen:
runs-on: ubuntu-latest
outputs:
subdomain: ${{ steps.gen.outputs.subdomain }}
address: ${{ steps.gen.outputs.address }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- id: gen
run: |
UUID=$(uuidgen)
echo "subdomain=fleet-test-$UUID" >> $GITHUB_OUTPUT
echo "address=https://fleet-test-$UUID.fleetuem.com" >> $GITHUB_OUTPUT
run-server:
runs-on: ubuntu-latest
needs: gen
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Start tunnel
env:
CERT_PEM: ${{ secrets.CLOUDFLARE_TUNNEL_FLEETUEM_CERT_B64 }}
run: |
# Increase maximum receive buffer size to roughly 2.5 MB.
# Cloudflared uses quic-go. This buffer holds packets that have been received by the kernel,
# but not yet read by the application (quic-go in this case). Once this buffer fills up, the
# kernel will drop any new incoming packet.
# See https://github.com/quic-go/quic-go/wiki/UDP-Receive-Buffer-Size.
sudo sysctl -w net.core.rmem_max=2500000
# Install cloudflared
#
# We pin to version 2025.5.0 because something broke with 2025.6.1.
# 2025.6.1 fails with "failed to create tunnel: Unknown output format 'default'"
wget https://github.com/cloudflare/cloudflared/releases/download/2025.5.0/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
# Add secret
echo "$CERT_PEM" | base64 -d > cert.pem
# Start tunnel
cloudflared tunnel --origincert cert.pem --hostname ${{ needs.gen.outputs.subdomain }} --url http://localhost:1337 --name ${{ needs.gen.outputs.subdomain }} --logfile cloudflared.log &
until [[ $(cloudflared tunnel --origincert cert.pem info -o json ${{ needs.gen.outputs.subdomain }} | jq '.conns[0].conns[0].is_pending_reconnect') = false ]]; do
echo "Awaiting tunnel ready..."
sleep 5
done
# Download fleet and fleetctl binaries from last successful build on main
- name: Download binaries
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67
with:
workflow: build-binaries.yaml
branch: main
name: build
path: build
check_artifacts: true
- name: Run Fleet server
timeout-minutes: 15
run: |
chmod +x ./build/fleetctl
./build/fleetctl preview --no-hosts --disable-open-browser
./build/fleetctl config set --address ${{ needs.gen.outputs.address }}
./build/fleetctl get enroll-secret
docker compose -f ~/.fleet/preview/docker-compose.yml logs --follow fleet01 fleet02 &
# Ensure Fleet server is responding before waiting for enrollments
echo "Checking Fleet server health..."
HEALTH_CHECK_COUNT=0
until curl -s -o /dev/null -w "%{http_code}" http://localhost:1337/healthz | grep -q "200"; do
HEALTH_CHECK_COUNT=$((HEALTH_CHECK_COUNT + 1))
if [ $HEALTH_CHECK_COUNT -ge 30 ]; then
echo "ERROR: Fleet server not responding after 150 seconds"
docker ps -a --filter "name=fleet"
exit 1
fi
sleep 5
done
echo "Fleet server is responding"
# Wait for all of the hosts to be enrolled
EXPECTED=3
ENROLLMENT_START=$(date +%s)
until [ $(./build/fleetctl get hosts --json | grep -v "No hosts found" | wc -l | tee hostcount) -ge $EXPECTED ]; do
CURRENT_TIME=$(date +%s)
ELAPSED=$((CURRENT_TIME - ENROLLMENT_START))
echo -n "Waiting for hosts to enroll (${ELAPSED}s): "
cat hostcount | xargs echo -n
echo " / $EXPECTED"
# Show diagnostic info every 60 seconds
if [ $((ELAPSED % 60)) -lt 10 ]; then
./build/fleetctl get hosts --json || true
fi
sleep 10
done
echo "Success! $EXPECTED hosts enrolled."
- name: Show enrolled hosts
if: always()
run: |
./build/fleetctl get hosts --json
- name: Slack Notification
if: failure()
uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0
with:
payload: |
{
"text": "${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head.html_url }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Integration test result: ${{ job.status }}\nhttps://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}\n${{ github.event.pull_request.html_url || github.event.head.html_url }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: Cleanup tunnel
if: always()
run: cloudflared tunnel --origincert cert.pem delete --force ${{ needs.gen.outputs.subdomain }}
- name: Upload cloudflared logs
if: always()
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: cloudflared.log
path: cloudflared.log
login:
runs-on: ubuntu-latest
needs: gen
outputs:
token: ${{ steps.login.outputs.token }}
steps:
# Download fleet and fleetctl binaries from last successful build on main
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Download binaries
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67
with:
workflow: build-binaries.yaml
branch: main
name: build
path: build
check_artifacts: true
# Login only here and share the token because otherwise we could hit rate limits.
- id: login
name: Attempt login
timeout-minutes: 5
run: |
chmod +x ./build/fleetctl
./build/fleetctl config set --address ${{ needs.gen.outputs.address }}
# Wait for Fleet server to be reachable first
echo "Waiting for Fleet server to be ready..."
ATTEMPT=0
until curl -s -o /dev/null -w "%{http_code}" ${{ needs.gen.outputs.address }}/healthz | grep -q "200"; do
ATTEMPT=$((ATTEMPT + 1))
if [ $ATTEMPT -ge 60 ]; then
echo "ERROR: Fleet server not reachable after 5 minutes"
exit 1
fi
echo "Waiting for server... attempt $ATTEMPT/60"
sleep 5
done
echo "Fleet server is responding, attempting login..."
ATTEMPT=0
until ./build/fleetctl login --email admin@example.com --password preview1337#; do
ATTEMPT=$((ATTEMPT + 1))
if [ $ATTEMPT -ge 30 ]; then
echo "ERROR: Failed to login after $ATTEMPT attempts"
exit 1
fi
echo "Login attempt $ATTEMPT failed, retrying in 5s..."
sleep 5
done
TOKEN=$(cat ~/.fleet/config| grep token | awk '{ print $2 }')
echo "token=$TOKEN" >> $GITHUB_OUTPUT
orbit-macos:
timeout-minutes: 10
strategy:
matrix:
# To run multiple VMs that have the same UUID we need to implement
# https://github.com/fleetdm/fleet/issues/8021 (otherwise orbit and osqueryd
# in the same host are enrolled as two hosts in Fleet).
# Until then we will just test the `stable` channel in all components.
#
# Alternatively, we can bring back the `edge` channel when we decide to upgrade
# our worker to macOS 13 in the future, as they changed the virtualization
# layer for 13 and now it has random UUIDs (https://github.com/actions/runner-images/issues/7591).
orbit-channel: [ 'stable' ]
osqueryd-channel: [ 'stable' ]
desktop-channel: [ 'stable' ]
runs-on: macos-latest
needs: [gen, login]
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout Code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Install dependencies
run: |
npm install -g fleetctl
fleetctl config set --address ${{ needs.gen.outputs.address }} --token ${{ needs.login.outputs.token }}
- name: Wait until fleet address is reachable and fleet responds
run: |
until curl -v -fail ${{ needs.gen.outputs.address }}/version;
do
echo "Awaiting until fleet server responds..."
sleep 10
done
- name: Install Orbit
run: |
sudo hostname macos-orbit-${{ matrix.orbit-channel }}-osqueryd-${{ matrix.osqueryd-channel }}
SECRET_JSON=$(fleetctl get enroll_secret --json --debug)
echo $SECRET_JSON
SECRET=$(echo $SECRET_JSON | jq -r '.spec.secrets[0].secret')
echo "Secret: $SECRET"
echo "Hostname: $(hostname -s)"
fleetctl package --type pkg --fleet-url=${{ needs.gen.outputs.address }} --enroll-secret=$SECRET --orbit-channel=${{ matrix.orbit-channel }} --osqueryd-channel=${{ matrix.osqueryd-channel }} --desktop-channel=${{ matrix.desktop-channel }} --fleet-desktop --debug
sudo installer -pkg fleet-osquery.pkg -target /
ENROLLMENT_START=$(date +%s)
until fleetctl get hosts | grep -iF $(hostname -s);
do
CURRENT_TIME=$(date +%s)
ELAPSED=$((CURRENT_TIME - ENROLLMENT_START))
echo "Awaiting enrollment... (${ELAPSED}s)"
sleep 10
done
- name: Collect orbit logs
if: always()
run: |
mkdir orbit-logs
sudo cp /var/log/orbit/* orbit-logs/
- name: Upload Orbit logs
if: always()
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: orbit-macos-${{ matrix.orbit-channel }}-${{ matrix.osqueryd-channel }}-${{ matrix.desktop-channel }}-logs
path: |
orbit-logs
- name: Uninstall Orbit
run: |
sudo ./it-and-security/lib/macos/scripts/uninstall-fleetd-macos.sh
orbit-ubuntu:
timeout-minutes: 10
strategy:
matrix:
# To run multiple VMs that have the same UUID we need to implement
# https://github.com/fleetdm/fleet/issues/8021 (otherwise orbit and osqueryd
# in the same host are enrolled as two hosts in Fleet).
# Until then we will just test the `stable` channel in all components.
orbit-channel: [ 'stable' ]
osqueryd-channel: [ 'stable' ]
desktop-channel: [ 'stable' ]
runs-on: ubuntu-latest
needs: [gen, login]
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Install dependencies
run: |
npm install -g fleetctl
fleetctl config set --address ${{ needs.gen.outputs.address }} --token ${{ needs.login.outputs.token }}
- name: Checkout Code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Install Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version-file: 'go.mod'
- name: Build Fleetctl
run: make fleetctl
- name: Wait until fleet address is reachable and fleet responds
run: |
until curl -v -fail ${{ needs.gen.outputs.address }}/version; do
echo "Awaiting until fleet server responds..."
sleep 10
done
- name: Install Orbit
run: |
sudo hostname ubuntu-orbit-${{ matrix.orbit-channel }}-osqueryd-${{ matrix.osqueryd-channel }}
chmod +x ./build/fleetctl
SECRET_JSON=$(fleetctl get enroll_secret --json --debug)
echo $SECRET_JSON
SECRET=$(echo $SECRET_JSON | jq -r '.spec.secrets[0].secret')
echo "Secret: $SECRET"
echo "Hostname: $(hostname -s)"
./build/fleetctl package --type deb --fleet-url=${{ needs.gen.outputs.address }} --enroll-secret=$SECRET --orbit-channel=${{ matrix.orbit-channel }} --osqueryd-channel=${{ matrix.osqueryd-channel }} --desktop-channel=${{ matrix.desktop-channel }} --fleet-desktop --debug
sudo dpkg -i fleet-osquery*
ENROLLMENT_START=$(date +%s)
until fleetctl get hosts | grep -iF $(hostname -s); do
CURRENT_TIME=$(date +%s)
ELAPSED=$((CURRENT_TIME - ENROLLMENT_START))
echo "Awaiting enrollment... (${ELAPSED}s)"
sudo systemctl status orbit.service || true
sleep 10
done
- name: Collect orbit logs
if: always()
run: |
sudo journalctl -u orbit.service > orbit-logs
- name: Upload Orbit logs
if: always()
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: orbit-ubuntu-${{ matrix.orbit-channel }}-${{ matrix.osqueryd-channel }}-${{ matrix.desktop-channel }}-logs
path: |
orbit-logs
- name: Uninstall Orbit
run: |
sudo apt remove fleet-osquery -y
orbit-windows-build:
timeout-minutes: 10
strategy:
matrix:
# To run multiple VMs that have the same UUID we need to implement
# https://github.com/fleetdm/fleet/issues/8021 (otherwise orbit and osqueryd
# in the same host are enrolled as two hosts in Fleet).
# Until then we will just test the `stable` channel in all components.
orbit-channel: [ 'stable' ]
osqueryd-channel: [ 'stable' ]
desktop-channel: [ 'stable' ]
runs-on: ubuntu-latest
needs: [gen, login]
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Install dependencies
run: |
docker pull fleetdm/wix:latest &
npm install -g fleetctl
fleetctl config set --address ${{ needs.gen.outputs.address }} --token ${{ needs.login.outputs.token }}
- name: Wait until fleet address is reachable and fleet responds
run: |
until curl -v -fail ${{ needs.gen.outputs.address }}/version;
do
echo "Awaiting until fleet server responds..."
sleep 10
done
- name: Build Orbit
run: |
SECRET_JSON=$(fleetctl get enroll_secret --json --debug)
echo $SECRET_JSON
SECRET=$(echo $SECRET_JSON | jq -r '.spec.secrets[0].secret')
echo "Secret: $SECRET"
echo "Hostname: $(hostname -s)"
fleetctl package --type msi --fleet-url=${{ needs.gen.outputs.address }} --enroll-secret=$SECRET --orbit-channel=${{ matrix.orbit-channel }} --osqueryd-channel=${{ matrix.osqueryd-channel }} --desktop-channel=${{ matrix.desktop-channel }} --fleet-desktop --debug
mv fleet-osquery.msi orbit-${{ matrix.orbit-channel }}-osqueryd-${{ matrix.osqueryd-channel }}-desktop-${{ matrix.desktop-channel }}.msi
- name: Upload MSI
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: orbit-${{ matrix.orbit-channel }}-osqueryd-${{ matrix.osqueryd-channel }}-desktop-${{ matrix.desktop-channel }}.msi
path: orbit-${{ matrix.orbit-channel }}-osqueryd-${{ matrix.osqueryd-channel }}-desktop-${{ matrix.desktop-channel }}.msi
orbit-windows:
timeout-minutes: 10
strategy:
matrix:
# To run multiple VMs that have the same UUID we need to implement
# https://github.com/fleetdm/fleet/issues/8021 (otherwise orbit and osqueryd
# in the same host are enrolled as two hosts in Fleet).
# Until then we will just test the `stable` channel in all components.
orbit-channel: [ 'stable' ]
osqueryd-channel: [ 'stable' ]
desktop-channel: [ 'stable' ]
needs: [gen, login, orbit-windows-build]
runs-on: windows-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Install dependencies
shell: bash
run: |
npm install -g fleetctl
fleetctl config set --address ${{ needs.gen.outputs.address }} --token ${{ needs.login.outputs.token }} --tls-skip-verify
- name: Download MSI
id: download
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: orbit-${{ matrix.orbit-channel }}-osqueryd-${{ matrix.osqueryd-channel }}-desktop-${{ matrix.desktop-channel }}.msi
- name: Install Orbit
shell: cmd
run: |
msiexec /i ${{steps.download.outputs.download-path}}\orbit-${{ matrix.orbit-channel }}-osqueryd-${{ matrix.osqueryd-channel }}-desktop-${{ matrix.desktop-channel }}.msi /quiet /passive /lv log.txt
sleep 120
# We can't very accurately check the install on these Windows hosts since the hostnames tend to
# overlap and we can't control the hostnames. Instead we just return and have the run-server job
# wait until the expected number of hosts enroll.
- name: Upload orbit install log
if: always()
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: msiexec-install-log
path: log.txt
- name: Upload Orbit logs
if: always()
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: orbit-windows-${{ matrix.orbit-channel }}-${{ matrix.osqueryd-channel }}-${{ matrix.desktop-channel }}-logs
path: C:\Windows\system32\config\systemprofile\AppData\Local\FleetDM\Orbit\Logs\orbit-osquery.log