Add workflows for validation on new FMAs only (#35888)

Currently none of our FMA validation runs are completing successfully.
With 100+ FMAs now available in our library. the workflow for validating
new apps is taking over an hour to run and prone to timeouts because it
validates all apps on every pull request, including checking Windows
apps when a new macOS app is submitted. These new workflows validate
only newly added FMAs while keeping the workflows for validating all
apps available for manual runs.

---------

Co-authored-by: Luke Heath <luke@fleetdm.com>
This commit is contained in:
Allen Houchins 2025-11-24 15:00:27 -06:00 committed by GitHub
parent d8b4cd90a5
commit 2bc8fb064d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 467 additions and 12 deletions

104
.github/scripts/detect-new-fmas-in-pr.sh vendored Executable file
View file

@ -0,0 +1,104 @@
#!/bin/bash
# Script to detect changed/new maintained apps in a PR
# This script compares the PR branch with the base branch to find:
# 1. New apps added to apps.json
# 2. Apps with changed manifest files
set -euo pipefail
# Get repository root
REPO_ROOT="${GITHUB_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
APPS_JSON="${REPO_ROOT}/ee/maintained-apps/outputs/apps.json"
OUTPUTS_DIR="${REPO_ROOT}/ee/maintained-apps/outputs"
# Base branch (usually main or the PR's base branch)
# In GitHub Actions, GITHUB_BASE_REF is set for pull_request events
BASE_BRANCH="${GITHUB_BASE_REF:-main}"
# Use origin/ prefix for remote branch reference
BASE_BRANCH_REF="origin/${BASE_BRANCH}"
# Check if jq is available
if ! command -v jq &> /dev/null; then
echo "Error: jq is required but not installed" >&2
exit 1
fi
# Function to extract app slugs from apps.json
extract_slugs() {
local apps_file="$1"
if [ ! -f "$apps_file" ]; then
echo ""
return
fi
jq -r '.apps[].slug' "$apps_file" | sort
}
# Function to extract app slugs from changed manifest files
extract_slugs_from_changed_manifests() {
local changed_files="$1"
local slugs=()
while IFS= read -r file; do
# Extract slug from path like: outputs/app-name/darwin.json or outputs/app-name/windows.json
if [[ "$file" =~ outputs/([^/]+)/(darwin|windows)\.json$ ]]; then
app_name="${BASH_REMATCH[1]}"
platform="${BASH_REMATCH[2]}"
slug="${app_name}/${platform}"
slugs+=("$slug")
fi
done <<< "$changed_files"
# Remove duplicates and sort
printf '%s\n' "${slugs[@]}" | sort -u
}
# Get changed files in outputs directory
echo "Detecting changed files in outputs directory..."
echo "Comparing HEAD with ${BASE_BRANCH_REF}..."
# Use merge-base to find the common ancestor for comparison
MERGE_BASE=$(git merge-base "${BASE_BRANCH_REF}" HEAD 2>/dev/null || echo "${BASE_BRANCH_REF}")
CHANGED_FILES=$(git diff --name-only "$MERGE_BASE" HEAD -- "ee/maintained-apps/outputs/" 2>/dev/null || echo "")
# Extract slugs from changed manifest files
CHANGED_MANIFEST_SLUGS=$(extract_slugs_from_changed_manifests "$CHANGED_FILES")
# Get current apps.json slugs
CURRENT_SLUGS=$(extract_slugs "$APPS_JSON")
# Get base branch apps.json slugs
echo "Fetching base branch apps.json from ${MERGE_BASE}..."
BASE_APPS_JSON=$(git show "${MERGE_BASE}:ee/maintained-apps/outputs/apps.json" 2>/dev/null || echo "")
BASE_SLUGS=""
if [ -n "$BASE_APPS_JSON" ]; then
BASE_SLUGS=$(echo "$BASE_APPS_JSON" | jq -r '.apps[].slug' | sort)
else
echo "Warning: Could not find apps.json in base branch, treating all current apps as new"
fi
# Find new slugs in apps.json
NEW_SLUGS=$(comm -13 <(echo "$BASE_SLUGS" || echo "") <(echo "$CURRENT_SLUGS" || echo "") || echo "")
# Combine all changed slugs (from manifest changes and new apps)
ALL_CHANGED_SLUGS=$(printf '%s\n' "$CHANGED_MANIFEST_SLUGS" "$NEW_SLUGS" | grep -v '^$' | sort -u)
# Output results
if [ -z "$ALL_CHANGED_SLUGS" ]; then
echo "No changed apps detected."
echo "CHANGED_APPS=" >> "$GITHUB_OUTPUT"
echo "HAS_CHANGES=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Detected changed apps:"
echo "$ALL_CHANGED_SLUGS" | while read -r slug; do
echo " - $slug"
done
# Output as JSON array for GitHub Actions
CHANGED_APPS_JSON=$(echo "$ALL_CHANGED_SLUGS" | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "CHANGED_APPS=$CHANGED_APPS_JSON" >> "$GITHUB_OUTPUT"
echo "HAS_CHANGES=true" >> "$GITHUB_OUTPUT"

38
.github/scripts/filter-apps-json.sh vendored Executable file
View file

@ -0,0 +1,38 @@
#!/bin/bash
# Script to filter apps.json to only include specified app slugs
# Usage: filter-apps-json.sh <slugs_json_array> <output_file>
set -euo pipefail
# Get repository root
REPO_ROOT="${GITHUB_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
APPS_JSON="${REPO_ROOT}/ee/maintained-apps/outputs/apps.json"
# Check if jq is available
if ! command -v jq &> /dev/null; then
echo "Error: jq is required but not installed" >&2
exit 1
fi
# Parse arguments
SLUGS_JSON="$1"
OUTPUT_FILE="$2"
if [ -z "$SLUGS_JSON" ] || [ "$SLUGS_JSON" == "[]" ] || [ "$SLUGS_JSON" == "null" ]; then
echo "No slugs provided, creating empty apps.json"
echo '{"version": 2, "apps": []}' > "$OUTPUT_FILE"
exit 0
fi
# Read the original apps.json
if [ ! -f "$APPS_JSON" ]; then
echo "Error: apps.json not found at $APPS_JSON" >&2
exit 1
fi
# Filter apps.json to only include the specified slugs
jq --argjson slugs "$SLUGS_JSON" '.apps = (.apps | map(select(.slug as $slug | $slugs | index($slug) != null)))' "$APPS_JSON" > "$OUTPUT_FILE"
echo "Filtered apps.json created with $(jq '.apps | length' "$OUTPUT_FILE") app(s)"

View file

@ -0,0 +1,145 @@
name: Test Fleet Maintained Apps - Darwin (PR Only)
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- ee/maintained-apps/inputs/**
- ee/maintained-apps/outputs/**
- cmd/maintained-apps/validate/**
workflow_dispatch: # Manual trigger
inputs:
log_level:
description: "Log level (debug, info, warn, error)"
required: false
default: "info"
type: choice
options:
- debug
- info
- warn
- error
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
test-fma-pr-only:
env:
LOG_LEVEL: ${{ github.event.inputs.log_level || 'info' }}
runs-on: macos-latest
steps:
- name: Checkout Fleet
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: fleetdm/fleet
fetch-depth: 0 # Need full history to compare with base branch
ref: ${{ github.ref }}
path: fleet
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: "fleet/go.mod"
- name: Fetch base branch
run: |
cd fleet
BASE_BRANCH="${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}"
echo "Fetching base branch: $BASE_BRANCH"
git fetch origin "$BASE_BRANCH:$BASE_BRANCH" || true
shell: bash
- name: Detect changed apps
id: detect-changed
env:
GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}
run: |
cd fleet
export GITHUB_WORKSPACE="$PWD"
.github/scripts/detect-new-fmas-in-pr.sh
shell: bash
- name: Check if there are changes
id: check-changes
run: |
if [ "${{ steps.detect-changed.outputs.HAS_CHANGES }}" == "true" ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "Changed apps detected: ${{ steps.detect-changed.outputs.CHANGED_APPS }}"
else
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "No changed apps detected, skipping validation"
fi
- name: Check if there are Darwin apps
id: check-darwin-apps
run: |
if [ "${{ steps.check-changes.outputs.has_changes }}" != "true" ]; then
echo "has_darwin_apps=false" >> $GITHUB_OUTPUT
exit 0
fi
# Filter changed apps to only include darwin platform
DARWIN_SLUGS=$(echo '${{ steps.detect-changed.outputs.CHANGED_APPS }}' | jq -r '.[] | select(endswith("/darwin"))')
if [ -z "$DARWIN_SLUGS" ]; then
echo "has_darwin_apps=false" >> $GITHUB_OUTPUT
echo "No darwin apps changed, skipping Darwin workflow"
else
echo "has_darwin_apps=true" >> $GITHUB_OUTPUT
echo "Darwin apps detected:"
echo "$DARWIN_SLUGS" | while read -r slug; do
echo " - $slug"
done
fi
shell: bash
- name: Install osquery mac
if: steps.check-darwin-apps.outputs.has_darwin_apps == 'true'
run: |
echo "Runner architecture: $(uname -m)"
curl -L -o osquery.tar.gz "https://github.com/osquery/osquery/releases/download/5.18.1/osquery-5.18.1_1.macos_arm64.tar.gz"
tar -xzf osquery.tar.gz
sudo cp -r opt /
sudo cp -r private /
sudo ln -sf /opt/osquery/lib/osquery.app/Contents/MacOS/osqueryd /usr/local/bin/osqueryi
sudo ln -sf /opt/osquery/lib/osquery.app/Contents/Resources/osqueryctl /usr/local/bin/osqueryctl
- name: Remove pre-installed google chrome mac
if: steps.check-darwin-apps.outputs.has_darwin_apps == 'true'
run: |
ls /Applications | grep -i "Chrome"
find /Applications -name "*Chrome*.app" -type d | while read app;
do
echo "Removing $app..."
sudo rm -rf "$app"
done
- name: Filter apps.json and verify changed apps
if: steps.check-darwin-apps.outputs.has_darwin_apps == 'true'
run: |
cd fleet
# Set GITHUB_WORKSPACE to current directory so scripts can find files
export GITHUB_WORKSPACE="$PWD"
# Filter changed apps to only include darwin platform
DARWIN_SLUGS=$(echo '${{ steps.detect-changed.outputs.CHANGED_APPS }}' | jq -r '.[] | select(endswith("/darwin"))')
DARWIN_SLUGS_JSON=$(echo "$DARWIN_SLUGS" | jq -R -s -c 'split("\n") | map(select(length > 0))')
# Backup original apps.json
cp ee/maintained-apps/outputs/apps.json ee/maintained-apps/outputs/apps.json.backup
# Create filtered apps.json
FILTERED_APPS_JSON=$(mktemp)
.github/scripts/filter-apps-json.sh "$DARWIN_SLUGS_JSON" "$FILTERED_APPS_JSON"
# Replace apps.json with filtered version
mv "$FILTERED_APPS_JSON" ee/maintained-apps/outputs/apps.json
# Run validation
ls /Applications
sudo -E go run ./cmd/maintained-apps/validate
# Restore original apps.json
mv ee/maintained-apps/outputs/apps.json.backup ee/maintained-apps/outputs/apps.json

View file

@ -1,12 +1,8 @@
name: Test Fleet Maintained Apps - Darwin
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- ee/maintained-apps/inputs/**
- ee/maintained-apps/outputs/**
- cmd/maintained-apps/validate/**
# Note: PR triggers removed - use test-fma-darwin-pr-only.yml for PRs
# This workflow is kept for manual testing of all FMAs via workflow_dispatch
workflow_dispatch: # Manual trigger
inputs:
log_level:

View file

@ -0,0 +1,176 @@
name: Test Fleet Maintained Apps - Windows (PR Only)
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- ee/maintained-apps/inputs/**
- ee/maintained-apps/outputs/**
- cmd/maintained-apps/validate/**
workflow_dispatch: # Manual trigger
inputs:
log_level:
description: "Log level (debug, info, warn, error)"
required: false
default: "info"
type: choice
options:
- debug
- info
- warn
- error
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
test-fma-pr-only:
env:
LOG_LEVEL: ${{ github.event.inputs.log_level || 'info' }}
runs-on: windows-latest
steps:
- name: Checkout Fleet
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: fleetdm/fleet
fetch-depth: 0 # Need full history to compare with base branch
ref: ${{ github.ref }}
path: fleet
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: "fleet/go.mod"
- name: Setup Git for base branch comparison
run: |
cd fleet
git config --global --add safe.directory $PWD
shell: pwsh
- name: Fetch base branch
run: |
cd fleet
$baseBranch = "${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}"
Write-Host "Fetching base branch: $baseBranch"
git fetch origin "$baseBranch`:$baseBranch" || exit 0
shell: pwsh
- name: Detect changed apps
id: detect-changed
env:
GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}
run: |
cd fleet
$env:GITHUB_WORKSPACE = (Get-Location).Path
bash .github/scripts/detect-new-fmas-in-pr.sh
shell: pwsh
- name: Check if there are changes
id: check-changes
run: |
if ("${{ steps.detect-changed.outputs.HAS_CHANGES }}" -eq "true") {
"has_changes=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "Changed apps detected: ${{ steps.detect-changed.outputs.CHANGED_APPS }}"
} else {
"has_changes=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "No changed apps detected, skipping validation"
}
shell: pwsh
- name: Check if there are Windows apps
id: check-windows-apps
run: |
if ("${{ steps.check-changes.outputs.has_changes }}" -ne "true") {
"has_windows_apps=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
exit 0
}
# Filter changed apps to only include windows platform
$changedAppsJson = '${{ steps.detect-changed.outputs.CHANGED_APPS }}'
$windowsSlugs = ($changedAppsJson | ConvertFrom-Json | Where-Object { $_ -like "*/windows" })
if ($null -eq $windowsSlugs -or $windowsSlugs.Count -eq 0) {
"has_windows_apps=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "No windows apps changed, skipping Windows workflow"
} else {
"has_windows_apps=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "Windows apps detected:"
$windowsSlugs | ForEach-Object { Write-Host " - $_" }
}
shell: pwsh
- name: Install osquery windows
if: steps.check-windows-apps.outputs.has_windows_apps == 'true'
run: |
Write-Host "Runner architecture: $env:PROCESSOR_ARCHITECTURE"
curl -L -o osquery.zip "https://github.com/osquery/osquery/releases/download/5.18.1/osquery-5.18.1.windows_x86_64.zip"
Expand-Archive -Path osquery.zip -DestinationPath osquery
Get-ChildItem -Recurse osquery | Where-Object { $_.Name -like "*osquery*" -and $_.Extension -eq ".exe" }
$osqueryPath = (Get-ChildItem -Recurse osquery | Where-Object { $_.Name -eq "osqueryi.exe" }).Directory.FullName
echo "Adding to PATH: $osqueryPath"
echo $osqueryPath | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
shell: pwsh
- name: Remove pre-installed google chrome
if: steps.check-windows-apps.outputs.has_windows_apps == 'true'
run: |
Write-Host "Listing all installed packages containing 'Chrome':"
Get-Package | Where-Object { $_.Name -like "*Chrome*" } | ForEach-Object {
Write-Host " - $($_.Name) (Version: $($_.Version))"
}
$uninstallPath = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like "*Google Chrome*" } | Select-Object -ExpandProperty UninstallString
if ($uninstallPath) {
Write-Host "Found Chrome uninstall path: $uninstallPath"
try {
$guid = ($uninstallPath -split "/X")[1]
Write-Host "Uninstalling Chrome MSI with GUID: $guid"
Start-Process -FilePath "msiexec.exe" -ArgumentList "/X$guid", "/quiet", "/norestart" -Wait -NoNewWindow
Write-Host "Successfully removed Google Chrome via MSI uninstaller"
} catch {
Write-Host "Failed to remove Chrome: $($_.Exception.Message)"
}
} else {
Write-Host "Chrome uninstall path not found in registry"
}
shell: pwsh
- name: Filter apps.json and verify changed apps
if: steps.check-windows-apps.outputs.has_windows_apps == 'true'
run: |
cd fleet
# Set GITHUB_WORKSPACE to current directory so scripts can find files
$env:GITHUB_WORKSPACE = (Get-Location).Path
# Filter changed apps to only include windows platform
$changedAppsJson = '${{ steps.detect-changed.outputs.CHANGED_APPS }}'
$windowsSlugs = ($changedAppsJson | ConvertFrom-Json | Where-Object { $_ -like "*/windows" })
$windowsSlugsJson = ($windowsSlugs | ConvertTo-Json -Compress)
Write-Host "Filtering apps.json for slugs: $windowsSlugsJson"
# Backup original apps.json
Copy-Item -Path "ee\maintained-apps\outputs\apps.json" -Destination "ee\maintained-apps\outputs\apps.json.backup"
# Create filtered apps.json
# Use a fixed path for the temp file to avoid issues with bash
$filteredAppsJson = Join-Path $env:TEMP "filtered-apps-$(New-Guid).json"
bash .github/scripts/filter-apps-json.sh "$windowsSlugsJson" "$filteredAppsJson"
# Verify the filtered file was created
if (-not (Test-Path $filteredAppsJson)) {
Write-Host "Error: Filtered apps.json was not created at $filteredAppsJson"
exit 1
}
# Replace apps.json with filtered version
Move-Item -Path $filteredAppsJson -Destination "ee\maintained-apps\outputs\apps.json" -Force
# Run validation
ls "C:\Program Files"
go run ./cmd/maintained-apps/validate
# Restore original apps.json
Move-Item -Path "ee\maintained-apps\outputs\apps.json.backup" -Destination "ee\maintained-apps\outputs\apps.json" -Force
shell: pwsh

View file

@ -1,12 +1,8 @@
name: Test Fleet Maintained Apps - Windows
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- ee/maintained-apps/inputs/**
- ee/maintained-apps/outputs/**
- cmd/maintained-apps/validate/**
# Note: PR triggers removed - use test-fma-windows-pr-only.yml for PRs
# This workflow is kept for manual testing of all FMAs via workflow_dispatch
workflow_dispatch: # Manual trigger
inputs:
log_level: