Shard FMA validation workflows and route Windows apps to arch-matched runners (#49348)
**Related issue:** N/A — CI improvement for the FMA validation workflows. ## Summary Restructures the Windows and macOS Fleet-maintained app validation workflows around a cheap Linux detect/shard job, with Windows apps additionally routed to a CI runner whose native architecture matches the app's installer. **Both platforms:** - Change detection and sharding run on `ubuntu-latest`. Expensive Windows/macOS runners only spin up when their platform actually has changed apps — e.g. a Windows-only letter-batch PR no longer boots a macOS runner just to discover there's nothing to do (and vice versa) — and they check out at depth 1 instead of full history. - A new `.github/scripts/partition-fma-apps.sh <windows|darwin>` emits the job matrix; validation steps move unchanged into reusable workflows (`test-fma-windows-validate.yml`, `test-fma-darwin-validate.yml`). - Large PRs shard into parallel jobs (Windows: 25 apps/shard, macOS: 30), and the manual full-run workflows gain a `shard_size` input (Windows default 20 → ~20 shards over 384 apps; macOS default 25 → ~39 shards over 961 apps). Neither full run could previously finish: hundreds of sequential installs blow the 6-hour job limit. - Pre-installed app handling is computed per shard from that shard's slug list — Windows removals (Chrome, 7-Zip, Firefox, Node.js, PowerShell, R, Git) and macOS steps (Chrome, Xcode for Icon Composer, the Fleet Desktop MDM config stub) only run on the runner validating that app. This also brings the full-run workflows to parity with the PR gates (they previously only removed Chrome). - Stable summary jobs (`test-fma-pr-only`, `test-fma`) aggregate the dynamic matrix results so branch protection / PR gating keeps a fixed check name. **Windows arch routing:** - Each changed `<name>/windows` slug's `installer_arch` is read from `ee/maintained-apps/inputs/winget/<name>.json`: `arm64` apps → `windows-11-arm`, x64/x86/neutral apps → `windows-latest` (x64). Missing input files default to x64 with a warning. This fixes installers that check the native OS architecture and abort under Prism emulation on the ARM runner (Inno Setup `ArchitecturesAllowed=x64` — GOG Galaxy, Reqable — and Docker Desktop). Future arm64 FMAs need no workflow change — `installer_arch: arm64` in the winget input is enough. - macOS needs no arch matrix: `macos-latest` is arm64 and x86-only casks run under Rosetta 2, which matches how customer Macs run them. # Checklist for submitter - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] QA'd all new/changed functionality manually Manual QA: - Partition script is shellcheck-clean and tested against the real repo for both platforms: empty input, mixed-platform slug lists, x86/neutral routing to the x64 runner, single-slug arrays, missing input file fallback, arm64/x64 split with sharding (via a synthetic arm64 input), invalid platform/shard-size rejection, and full-catalog partitions (384 Windows apps → 20 shards, 961 darwin apps → 39 shards, all slugs accounted for, matrix outputs well under the 1 MB job-output limit). - All six workflows pass `actionlint` and zizmor 1.25.2 (with the repo's `.github/zizmor-gate.yml` config) with no findings. - The rewritten Windows PR gate ran on this PR itself: the Linux detect job correctly found no changed Windows apps, skipped the Windows runners, and the `test-fma-pr-only` summary check passed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added sharded validation for maintained macOS and Windows apps to run tests in parallel. * Added configurable `shard_size` for manual validation runs. * Introduced reusable validation workflows for Darwin and Windows. * Improved Windows testing to be architecture-aware (ARM64 vs x64). * **Bug Fixes** * Improved pull request gating to validate only changed apps and report results more reliably. * Workflows now gracefully handle scenarios where no matching apps are found (avoid unnecessary failures). <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
d59c5b82fa
commit
9b5fc40b2e
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Partition FMA slugs for a platform into a GitHub Actions job matrix whose
|
||||
# entries route apps to an appropriate runner, splitting buckets larger than
|
||||
# the shard size into shards that validate in parallel.
|
||||
#
|
||||
# windows Apps are routed to a runner with the matching native installer
|
||||
# architecture: arm64 apps to windows-11-arm, everything else
|
||||
# (x64, x86, neutral) to the x64 runner. The architecture for a
|
||||
# slug like "7-zip/windows" is read from
|
||||
# ee/maintained-apps/inputs/winget/7-zip.json (.installer_arch).
|
||||
# Slugs whose input file or installer_arch is missing default to
|
||||
# the x64 runner.
|
||||
# darwin All apps run on macos-latest (arm64; x86-only casks run under
|
||||
# Rosetta 2, matching how customer Macs run them). No architecture
|
||||
# partitioning is needed.
|
||||
#
|
||||
# Usage: partition-fma-apps.sh <windows|darwin> <slugs_json_array | slugs_json_file> [shard_size]
|
||||
#
|
||||
# Like filter-apps-json.sh, the slugs argument is either a literal JSON array
|
||||
# string or a path to a file containing one. Slugs for other platforms are
|
||||
# ignored.
|
||||
#
|
||||
# Outputs (appended to $GITHUB_OUTPUT):
|
||||
# has_apps - "true" or "false"
|
||||
# matrix - JSON array of {name, runner, slugs} objects, where slugs is a
|
||||
# JSON-encoded array string for that shard. Windows entries also
|
||||
# carry an "arch" field.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
WINDOWS_X64_RUNNER="windows-latest"
|
||||
WINDOWS_ARM64_RUNNER="windows-11-arm"
|
||||
DARWIN_RUNNER="macos-latest"
|
||||
|
||||
REPO_ROOT="${GITHUB_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||
WINGET_INPUTS_DIR="${REPO_ROOT}/ee/maintained-apps/inputs/winget"
|
||||
GITHUB_OUTPUT="${GITHUB_OUTPUT:-/dev/stdout}"
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is required but not installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PLATFORM="${1:-}"
|
||||
SLUGS_INPUT="${2:-[]}"
|
||||
SHARD_SIZE="${3:-25}"
|
||||
|
||||
if [ "$PLATFORM" != "windows" ] && [ "$PLATFORM" != "darwin" ]; then
|
||||
echo "Error: platform must be 'windows' or 'darwin', got '$PLATFORM'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$SLUGS_INPUT" ] && [ -f "$SLUGS_INPUT" ]; then
|
||||
SLUGS_JSON="$(cat "$SLUGS_INPUT")"
|
||||
else
|
||||
SLUGS_JSON="$SLUGS_INPUT"
|
||||
fi
|
||||
if [ -z "$SLUGS_JSON" ] || [ "$SLUGS_JSON" == "null" ]; then
|
||||
SLUGS_JSON="[]"
|
||||
fi
|
||||
|
||||
if ! [[ "$SHARD_SIZE" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "Error: shard size must be a positive integer, got '$SHARD_SIZE'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PLATFORM_SLUGS_JSON=$(jq -c --arg suffix "/${PLATFORM}" '[.[] | select(endswith($suffix))] | unique' <<< "$SLUGS_JSON")
|
||||
TOTAL=$(jq 'length' <<< "$PLATFORM_SLUGS_JSON")
|
||||
|
||||
if [ "$TOTAL" -eq 0 ]; then
|
||||
echo "No ${PLATFORM} apps to validate."
|
||||
echo "has_apps=false" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix=[]" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ENTRIES_FILE="$(mktemp)"
|
||||
trap 'rm -f "$ENTRIES_FILE"' EXIT
|
||||
|
||||
# emit_shards <bucket_name> <runner> <arch> <slug...>
|
||||
# arch is embedded in the matrix entry when non-empty (windows only).
|
||||
emit_shards() {
|
||||
local bucket="$1" runner="$2" arch="$3"
|
||||
shift 3
|
||||
local slugs=("$@")
|
||||
local total=${#slugs[@]}
|
||||
[ "$total" -eq 0 ] && return 0
|
||||
local shards=$(( (total + SHARD_SIZE - 1) / SHARD_SIZE ))
|
||||
local i=0 shard=1
|
||||
while [ "$i" -lt "$total" ]; do
|
||||
local chunk=("${slugs[@]:$i:$SHARD_SIZE}")
|
||||
local chunk_json
|
||||
chunk_json=$(printf '%s\n' "${chunk[@]}" | jq -R . | jq -s -c .)
|
||||
local name="$bucket"
|
||||
if [ "$shards" -gt 1 ]; then
|
||||
name="$bucket (${shard}/${shards})"
|
||||
fi
|
||||
jq -c -n --arg name "$name" --arg runner "$runner" --arg arch "$arch" --arg slugs "$chunk_json" \
|
||||
'{name: $name, runner: $runner, slugs: $slugs} + (if $arch != "" then {arch: $arch} else {} end)' >> "$ENTRIES_FILE"
|
||||
i=$((i + SHARD_SIZE))
|
||||
shard=$((shard + 1))
|
||||
done
|
||||
}
|
||||
|
||||
case "$PLATFORM" in
|
||||
windows)
|
||||
x64_slugs=()
|
||||
arm64_slugs=()
|
||||
while IFS= read -r slug; do
|
||||
[ -z "$slug" ] && continue
|
||||
name="${slug%/windows}"
|
||||
input_file="${WINGET_INPUTS_DIR}/${name}.json"
|
||||
arch=""
|
||||
if [ -f "$input_file" ]; then
|
||||
arch=$(jq -r '.installer_arch // empty' "$input_file" 2>/dev/null || echo "")
|
||||
else
|
||||
echo "Warning: no winget input file for '$slug' at $input_file, assuming x64" >&2
|
||||
fi
|
||||
case "$arch" in
|
||||
arm64)
|
||||
arm64_slugs+=("$slug")
|
||||
;;
|
||||
*)
|
||||
# x64, x86 and neutral installers all run natively on the x64 runner.
|
||||
x64_slugs+=("$slug")
|
||||
;;
|
||||
esac
|
||||
echo " - $slug -> ${arch:-x64}"
|
||||
done < <(jq -r '.[]' <<< "$PLATFORM_SLUGS_JSON")
|
||||
|
||||
emit_shards "x64" "$WINDOWS_X64_RUNNER" "x64" ${x64_slugs[@]+"${x64_slugs[@]}"}
|
||||
emit_shards "arm64" "$WINDOWS_ARM64_RUNNER" "arm64" ${arm64_slugs[@]+"${arm64_slugs[@]}"}
|
||||
|
||||
echo "Windows apps to validate: $TOTAL (x64/x86/neutral: ${#x64_slugs[@]}, arm64: ${#arm64_slugs[@]})"
|
||||
;;
|
||||
darwin)
|
||||
darwin_slugs=()
|
||||
while IFS= read -r slug; do
|
||||
[ -z "$slug" ] && continue
|
||||
darwin_slugs+=("$slug")
|
||||
echo " - $slug"
|
||||
done < <(jq -r '.[]' <<< "$PLATFORM_SLUGS_JSON")
|
||||
|
||||
emit_shards "darwin" "$DARWIN_RUNNER" "" ${darwin_slugs[@]+"${darwin_slugs[@]}"}
|
||||
|
||||
echo "Darwin apps to validate: $TOTAL"
|
||||
;;
|
||||
esac
|
||||
|
||||
MATRIX_JSON=$(jq -c -s . "$ENTRIES_FILE")
|
||||
echo "Matrix: $MATRIX_JSON"
|
||||
|
||||
echo "has_apps=true" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix=${MATRIX_JSON}" >> "$GITHUB_OUTPUT"
|
||||
@@ -7,6 +7,11 @@ on:
|
||||
- ee/maintained-apps/inputs/**
|
||||
- ee/maintained-apps/outputs/**
|
||||
- cmd/maintained-apps/validate/**
|
||||
- .github/workflows/test-fma-darwin-pr-only.yml
|
||||
- .github/workflows/test-fma-darwin-validate.yml
|
||||
- .github/scripts/partition-fma-apps.sh
|
||||
- .github/scripts/detect-new-fmas-in-pr.sh
|
||||
- .github/scripts/filter-apps-json.sh
|
||||
workflow_dispatch: # Manual trigger
|
||||
inputs:
|
||||
log_level:
|
||||
@@ -20,18 +25,20 @@ on:
|
||||
- warn
|
||||
- error
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-fma-pr-only:
|
||||
env:
|
||||
LOG_LEVEL: ${{ github.event.inputs.log_level || 'info' }}
|
||||
runs-on: macos-latest
|
||||
|
||||
# Detect which apps changed and shard them on a cheap Linux runner. The
|
||||
# (much more expensive) macOS runners below only spin up when there are
|
||||
# darwin apps to validate — Windows-only FMA PRs no longer boot a macOS
|
||||
# runner just to discover there is nothing to do. Large PRs are split into
|
||||
# shards that validate in parallel.
|
||||
detect-changed-apps:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has_darwin_apps: ${{ steps.partition.outputs.has_apps }}
|
||||
matrix: ${{ steps.partition.outputs.matrix }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
@@ -41,185 +48,69 @@ jobs:
|
||||
- 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
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.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
|
||||
# fetch-depth 0 normally brings in the base branch already; fetch it
|
||||
# explicitly as a fallback so origin/$GITHUB_BASE_REF exists.
|
||||
git fetch origin "$GITHUB_BASE_REF" || true
|
||||
bash .github/scripts/detect-new-fmas-in-pr.sh
|
||||
|
||||
- name: Check if there are changes
|
||||
id: check-changes
|
||||
- name: Shard Darwin apps
|
||||
id: partition
|
||||
env:
|
||||
CHANGED_APPS: ${{ steps.detect-changed.outputs.CHANGED_APPS }}
|
||||
run: |
|
||||
# Default to no changes if detection step failed or didn't set output
|
||||
HAS_CHANGES="${{ steps.detect-changed.outputs.HAS_CHANGES }}"
|
||||
if [ "$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"
|
||||
bash .github/scripts/partition-fma-apps.sh darwin "${CHANGED_APPS:-[]}" 30
|
||||
|
||||
validate:
|
||||
needs: detect-changed-apps
|
||||
if: needs.detect-changed-apps.outputs.has_darwin_apps == 'true'
|
||||
name: ${{ matrix.name }}
|
||||
strategy:
|
||||
# Don't cancel the other shards' validation if one fails.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.detect-changed-apps.outputs.matrix) }}
|
||||
uses: ./.github/workflows/test-fma-darwin-validate.yml
|
||||
permissions:
|
||||
contents: read
|
||||
with:
|
||||
runner: ${{ matrix.runner }}
|
||||
slugs: ${{ matrix.slugs }}
|
||||
log_level: ${{ github.event.inputs.log_level || 'info' }}
|
||||
|
||||
# Stable-named summary check (matches the old single-job name) so PR gating
|
||||
# doesn't depend on the dynamic per-shard matrix job names.
|
||||
test-fma-pr-only:
|
||||
needs: [detect-changed-apps, validate]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check validation results
|
||||
env:
|
||||
DETECT_RESULT: ${{ needs.detect-changed-apps.result }}
|
||||
VALIDATE_RESULT: ${{ needs.validate.result }}
|
||||
run: |
|
||||
echo "detect-changed-apps: $DETECT_RESULT"
|
||||
echo "validate: $VALIDATE_RESULT"
|
||||
if [ "$DETECT_RESULT" != "success" ]; then
|
||||
echo "Detecting changed apps failed"
|
||||
exit 1
|
||||
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
|
||||
echo "has_google_chrome=false" >> $GITHUB_OUTPUT
|
||||
echo "has_icon_composer=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
# validate is skipped when the PR changes no darwin apps.
|
||||
if [ "$VALIDATE_RESULT" != "success" ] && [ "$VALIDATE_RESULT" != "skipped" ]; then
|
||||
echo "Validation failed"
|
||||
exit 1
|
||||
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 "has_google_chrome=false" >> $GITHUB_OUTPUT
|
||||
echo "has_fleet_desktop=false" >> $GITHUB_OUTPUT
|
||||
echo "has_icon_composer=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
|
||||
|
||||
# Check if google-chrome/darwin is in the changed apps
|
||||
if echo "$DARWIN_SLUGS" | grep -q "^google-chrome/darwin$"; then
|
||||
echo "has_google_chrome=true" >> $GITHUB_OUTPUT
|
||||
echo "Google Chrome detected in changed apps"
|
||||
else
|
||||
echo "has_google_chrome=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Check if fleet-desktop/darwin is in the changed apps
|
||||
if echo "$DARWIN_SLUGS" | grep -q "^fleet-desktop/darwin$"; then
|
||||
echo "has_fleet_desktop=true" >> $GITHUB_OUTPUT
|
||||
echo "Fleet Desktop detected in changed apps"
|
||||
else
|
||||
echo "has_fleet_desktop=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Check if icon-composer/darwin is in the changed apps
|
||||
if echo "$DARWIN_SLUGS" | grep -q "^icon-composer/darwin$"; then
|
||||
echo "has_icon_composer=true" >> $GITHUB_OUTPUT
|
||||
echo "Icon Composer detected in changed apps"
|
||||
else
|
||||
echo "has_icon_composer=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
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' && steps.check-darwin-apps.outputs.has_google_chrome == '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
|
||||
|
||||
# Icon Composer ships bundled inside Xcode, and GitHub macOS runners
|
||||
# come with Xcode pre-installed. Remove it so the Icon Composer FMA
|
||||
# install script is validated against a clean install rather than an
|
||||
# already-present copy. Only runs when icon-composer/darwin is being
|
||||
# validated in this PR.
|
||||
- name: Remove pre-installed Xcode mac
|
||||
if: steps.check-darwin-apps.outputs.has_darwin_apps == 'true' && steps.check-darwin-apps.outputs.has_icon_composer == 'true'
|
||||
run: |
|
||||
ls /Applications | grep -i "Xcode" || true
|
||||
find /Applications -maxdepth 1 -iname "Xcode*.app" -type d | while read app;
|
||||
do
|
||||
echo "Removing $app..."
|
||||
sudo rm -rf "$app"
|
||||
done
|
||||
|
||||
# Fleet Desktop's installer refuses to run unless the
|
||||
# com.fleetdm.fleetd.config managed preferences profile is present
|
||||
# (it's normally delivered via MDM). CI runners aren't MDM-enrolled,
|
||||
# so we drop a stub plist in place before the validate step so the
|
||||
# install script succeeds. This only runs when fleet-desktop/darwin
|
||||
# is actually being validated in this PR.
|
||||
- name: Create Fleet Desktop MDM config stub (CI-only)
|
||||
if: steps.check-darwin-apps.outputs.has_darwin_apps == 'true' && steps.check-darwin-apps.outputs.has_fleet_desktop == 'true'
|
||||
run: |
|
||||
sudo mkdir -p "/Library/Managed Preferences"
|
||||
sudo tee "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist" > /dev/null <<'PLIST'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EnrollSecret</key>
|
||||
<string>ci-test-placeholder</string>
|
||||
<key>FleetURL</key>
|
||||
<string>https://ci.test.example.com</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
sudo chmod 644 "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist"
|
||||
ls -l "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist"
|
||||
|
||||
- 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
|
||||
echo "All Darwin FMA validations passed"
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
# Reusable workflow that installs and validates a set of macOS Fleet-maintained
|
||||
# apps. Called by test-fma-darwin-pr-only.yml and test-fma-darwin.yml with a
|
||||
# matrix produced by .github/scripts/partition-fma-apps.sh. All darwin apps run
|
||||
# on the arm64 macos-latest runner (x86-only casks run under Rosetta 2, which
|
||||
# matches how customer Macs run them), so unlike Windows there is no
|
||||
# per-architecture routing.
|
||||
name: Validate Fleet Maintained Apps - Darwin
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
runner:
|
||||
description: 'Runner label to validate on (e.g. "macos-latest")'
|
||||
required: true
|
||||
type: string
|
||||
slugs:
|
||||
description: 'JSON array of app slugs to validate (e.g. ["box-drive/darwin"])'
|
||||
required: true
|
||||
type: string
|
||||
log_level:
|
||||
description: "Log level (debug, info, warn, error)"
|
||||
required: false
|
||||
type: string
|
||||
default: "info"
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LOG_LEVEL: ${{ inputs.log_level }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ${{ inputs.runner }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
# Changed-app detection and sharding happen in the calling workflow on a
|
||||
# Linux runner, so no git history is needed here.
|
||||
- name: Checkout Fleet
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
path: fleet
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version-file: "fleet/go.mod"
|
||||
|
||||
- name: Determine pre-installed apps to remove
|
||||
id: check-darwin-apps
|
||||
# Pass the slugs through env rather than expanding ${{ inputs.slugs }}
|
||||
# into the script body (flagged by zizmor as template injection).
|
||||
env:
|
||||
SLUGS_JSON: ${{ inputs.slugs }}
|
||||
run: |
|
||||
echo "Apps to validate on this $(uname -m) runner:"
|
||||
echo "$SLUGS_JSON" | jq -r '.[] | " - \(.)"'
|
||||
|
||||
# The runner images ship with some of the apps we validate already
|
||||
# installed (or bundled, in Icon Composer's case); flag the ones
|
||||
# present in this shard so the steps below start the validator from
|
||||
# a clean state.
|
||||
has_flag() {
|
||||
echo "$SLUGS_JSON" | jq -e --arg slug "$1" 'index($slug) != null' > /dev/null && echo "true" || echo "false"
|
||||
}
|
||||
{
|
||||
echo "has_google_chrome=$(has_flag 'google-chrome/darwin')"
|
||||
echo "has_icon_composer=$(has_flag 'icon-composer/darwin')"
|
||||
echo "has_fleet_desktop=$(has_flag 'fleet-desktop/darwin')"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
|
||||
- name: Install osquery mac
|
||||
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_google_chrome == 'true'
|
||||
run: |
|
||||
find /Applications -maxdepth 1 -iname "*chrome*"
|
||||
find /Applications -name "*Chrome*.app" -type d | while read -r app;
|
||||
do
|
||||
echo "Removing $app..."
|
||||
sudo rm -rf "$app"
|
||||
done
|
||||
|
||||
# Icon Composer ships bundled inside Xcode, and GitHub macOS runners
|
||||
# come with Xcode pre-installed. Remove it so the Icon Composer FMA
|
||||
# install script is validated against a clean install rather than an
|
||||
# already-present copy. Only runs when icon-composer/darwin is being
|
||||
# validated in this shard.
|
||||
- name: Remove pre-installed Xcode mac
|
||||
if: steps.check-darwin-apps.outputs.has_icon_composer == 'true'
|
||||
run: |
|
||||
find /Applications -maxdepth 1 -iname "Xcode*"
|
||||
find /Applications -maxdepth 1 -iname "Xcode*.app" -type d | while read -r app;
|
||||
do
|
||||
echo "Removing $app..."
|
||||
sudo rm -rf "$app"
|
||||
done
|
||||
|
||||
# Fleet Desktop's installer refuses to run unless the
|
||||
# com.fleetdm.fleetd.config managed preferences profile is present
|
||||
# (it's normally delivered via MDM). CI runners aren't MDM-enrolled,
|
||||
# so we drop a stub plist in place before the validate step so the
|
||||
# install script succeeds. This only runs when fleet-desktop/darwin
|
||||
# is actually being validated in this shard.
|
||||
- name: Create Fleet Desktop MDM config stub (CI-only)
|
||||
if: steps.check-darwin-apps.outputs.has_fleet_desktop == 'true'
|
||||
run: |
|
||||
sudo mkdir -p "/Library/Managed Preferences"
|
||||
sudo tee "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist" > /dev/null <<'PLIST'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EnrollSecret</key>
|
||||
<string>ci-test-placeholder</string>
|
||||
<key>FleetURL</key>
|
||||
<string>https://ci.test.example.com</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
sudo chmod 644 "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist"
|
||||
ls -l "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist"
|
||||
|
||||
- name: Filter apps.json and validate apps
|
||||
# Pass the slugs through env rather than expanding ${{ inputs.slugs }}
|
||||
# into the script body (flagged by zizmor as template injection).
|
||||
env:
|
||||
SLUGS_JSON: ${{ inputs.slugs }}
|
||||
run: |
|
||||
cd fleet
|
||||
# Set GITHUB_WORKSPACE to current directory so scripts can find files
|
||||
export GITHUB_WORKSPACE="$PWD"
|
||||
|
||||
# The shard's slugs arrive as a compact JSON array string built by
|
||||
# the partition script, ready for filter-apps-json.sh as-is.
|
||||
echo "Filtering apps.json for slugs: $SLUGS_JSON"
|
||||
|
||||
# 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 "$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
|
||||
@@ -15,19 +15,25 @@ on:
|
||||
- info
|
||||
- warn
|
||||
- error
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shard_size:
|
||||
description: "Maximum number of apps per validation job (shards run in parallel)"
|
||||
required: false
|
||||
default: "20"
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-fma:
|
||||
env:
|
||||
LOG_LEVEL: ${{ github.event.inputs.log_level || 'info' }}
|
||||
runs-on: macos-latest
|
||||
|
||||
# Shard every darwin app in apps.json on a cheap Linux runner, then fan out
|
||||
# to macOS runners that validate the shards in parallel. Note that GitHub
|
||||
# caps concurrent macOS jobs well below Linux/Windows, so shards beyond the
|
||||
# cap queue and run in waves.
|
||||
partition:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has_darwin_apps: ${{ steps.partition.outputs.has_apps }}
|
||||
matrix: ${{ steps.partition.outputs.matrix }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
@@ -37,74 +43,59 @@ jobs:
|
||||
- name: Checkout Fleet
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: fleetdm/fleet
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.ref }}
|
||||
path: fleet
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
- name: Shard all Darwin apps
|
||||
id: partition
|
||||
env:
|
||||
SHARD_SIZE: ${{ github.event.inputs.shard_size || '25' }}
|
||||
run: |
|
||||
ALL_DARWIN_SLUGS=$(jq -c '[.apps[].slug | select(endswith("/darwin"))]' ee/maintained-apps/outputs/apps.json)
|
||||
bash .github/scripts/partition-fma-apps.sh darwin "$ALL_DARWIN_SLUGS" "$SHARD_SIZE"
|
||||
|
||||
validate:
|
||||
needs: partition
|
||||
if: needs.partition.outputs.has_darwin_apps == 'true'
|
||||
name: ${{ matrix.name }}
|
||||
strategy:
|
||||
# Keep validating the remaining shards even if one fails so a full run
|
||||
# reports every broken app, not just the first.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.partition.outputs.matrix) }}
|
||||
uses: ./.github/workflows/test-fma-darwin-validate.yml
|
||||
permissions:
|
||||
contents: read
|
||||
with:
|
||||
runner: ${{ matrix.runner }}
|
||||
slugs: ${{ matrix.slugs }}
|
||||
log_level: ${{ github.event.inputs.log_level || 'info' }}
|
||||
|
||||
# Stable-named summary job aggregating the per-shard results.
|
||||
test-fma:
|
||||
needs: [partition, validate]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
with:
|
||||
go-version-file: "fleet/go.mod"
|
||||
egress-policy: audit
|
||||
|
||||
- name: Install osquery mac
|
||||
- name: Check validation results
|
||||
env:
|
||||
PARTITION_RESULT: ${{ needs.partition.result }}
|
||||
VALIDATE_RESULT: ${{ needs.validate.result }}
|
||||
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
|
||||
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
|
||||
|
||||
# Icon Composer ships bundled inside Xcode, and GitHub macOS runners
|
||||
# come with Xcode pre-installed. Only runs when icon-composer/darwin is being
|
||||
# validated in this PR.
|
||||
- name: Remove pre-installed Xcode mac
|
||||
run: |
|
||||
ls /Applications | grep -i "Xcode" || true
|
||||
find /Applications -maxdepth 1 -iname "Xcode*.app" -type d | while read app;
|
||||
do
|
||||
echo "Removing $app..."
|
||||
sudo rm -rf "$app"
|
||||
done
|
||||
|
||||
# Fleet Desktop's installer refuses to run unless the
|
||||
# com.fleetdm.fleetd.config managed preferences profile is present
|
||||
# (it's normally delivered via MDM). CI runners aren't MDM-enrolled,
|
||||
# so we drop a stub plist in place before the validate step so the
|
||||
# install script succeeds. This workflow validates every FMA every
|
||||
# run, so fleet-desktop is always exercised.
|
||||
- name: Create Fleet Desktop MDM config stub (CI-only)
|
||||
run: |
|
||||
sudo mkdir -p "/Library/Managed Preferences"
|
||||
sudo tee "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist" > /dev/null <<'PLIST'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EnrollSecret</key>
|
||||
<string>ci-test-placeholder</string>
|
||||
<key>FleetURL</key>
|
||||
<string>https://ci.test.example.com</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
sudo chmod 644 "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist"
|
||||
ls -l "/Library/Managed Preferences/com.fleetdm.fleetd.config.plist"
|
||||
|
||||
- name: Verify Fleet Maintained Apps mac
|
||||
run: |
|
||||
ls /Applications
|
||||
cd fleet
|
||||
sudo -E go run ./cmd/maintained-apps/validate
|
||||
echo "partition: $PARTITION_RESULT"
|
||||
echo "validate: $VALIDATE_RESULT"
|
||||
if [ "$PARTITION_RESULT" != "success" ]; then
|
||||
echo "Sharding apps failed"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$VALIDATE_RESULT" != "success" ] && [ "$VALIDATE_RESULT" != "skipped" ]; then
|
||||
echo "Validation failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "All Darwin FMA validations passed"
|
||||
|
||||
@@ -7,6 +7,11 @@ on:
|
||||
- ee/maintained-apps/inputs/**
|
||||
- ee/maintained-apps/outputs/**
|
||||
- cmd/maintained-apps/validate/**
|
||||
- .github/workflows/test-fma-windows-pr-only.yml
|
||||
- .github/workflows/test-fma-windows-validate.yml
|
||||
- .github/scripts/partition-fma-apps.sh
|
||||
- .github/scripts/detect-new-fmas-in-pr.sh
|
||||
- .github/scripts/filter-apps-json.sh
|
||||
workflow_dispatch: # Manual trigger
|
||||
inputs:
|
||||
log_level:
|
||||
@@ -20,20 +25,21 @@ on:
|
||||
- warn
|
||||
- error
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-fma-pr-only:
|
||||
env:
|
||||
LOG_LEVEL: ${{ github.event.inputs.log_level || 'info' }}
|
||||
# ARM64 runner so we can validate ARM-native FMAs. x86/x64 FMAs continue to
|
||||
# install and run here via Windows 11 on ARM's Prism emulation.
|
||||
runs-on: windows-11-arm
|
||||
|
||||
# Detect which apps changed and partition them by installer architecture on a
|
||||
# cheap Linux runner. The (much more expensive) Windows runners below only
|
||||
# spin up when there are Windows apps to validate, and each app is routed to
|
||||
# a runner whose native architecture matches its installer: arm64 apps to
|
||||
# windows-11-arm, x64/x86/neutral apps to the x64 runner. Large PRs are split
|
||||
# into shards that validate in parallel.
|
||||
detect-changed-apps:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has_windows_apps: ${{ steps.partition.outputs.has_apps }}
|
||||
matrix: ${{ steps.partition.outputs.matrix }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
@@ -43,720 +49,69 @@ jobs:
|
||||
- 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
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.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
|
||||
# fetch-depth 0 normally brings in the base branch already; fetch it
|
||||
# explicitly as a fallback so origin/$GITHUB_BASE_REF exists.
|
||||
git fetch origin "$GITHUB_BASE_REF" || true
|
||||
bash .github/scripts/detect-new-fmas-in-pr.sh
|
||||
shell: pwsh
|
||||
|
||||
- name: Check if there are changes
|
||||
id: check-changes
|
||||
- name: Partition Windows apps by architecture
|
||||
id: partition
|
||||
env:
|
||||
CHANGED_APPS: ${{ steps.detect-changed.outputs.CHANGED_APPS }}
|
||||
run: |
|
||||
# Default to no changes if detection step failed or didn't set output
|
||||
$hasChanges = "${{ steps.detect-changed.outputs.HAS_CHANGES }}"
|
||||
if ($hasChanges -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
|
||||
bash .github/scripts/partition-fma-apps.sh windows "${CHANGED_APPS:-[]}" 25
|
||||
|
||||
- name: Check if there are Windows apps
|
||||
id: check-windows-apps
|
||||
validate:
|
||||
needs: detect-changed-apps
|
||||
if: needs.detect-changed-apps.outputs.has_windows_apps == 'true'
|
||||
name: ${{ matrix.name }}
|
||||
strategy:
|
||||
# Don't cancel the other architecture's validation if one fails.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.detect-changed-apps.outputs.matrix) }}
|
||||
uses: ./.github/workflows/test-fma-windows-validate.yml
|
||||
permissions:
|
||||
contents: read
|
||||
with:
|
||||
runner: ${{ matrix.runner }}
|
||||
slugs: ${{ matrix.slugs }}
|
||||
log_level: ${{ github.event.inputs.log_level || 'info' }}
|
||||
|
||||
# Stable-named summary check (matches the old single-job name) so PR gating
|
||||
# doesn't depend on the dynamic per-architecture/per-shard matrix job names.
|
||||
test-fma-pr-only:
|
||||
needs: [detect-changed-apps, validate]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check validation results
|
||||
env:
|
||||
DETECT_RESULT: ${{ needs.detect-changed-apps.result }}
|
||||
VALIDATE_RESULT: ${{ needs.validate.result }}
|
||||
run: |
|
||||
if ("${{ steps.check-changes.outputs.has_changes }}" -ne "true") {
|
||||
"has_windows_apps=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_google_chrome=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_7zip=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_firefox=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_nodejs=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_powershell=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_r=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_git=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
|
||||
"has_google_chrome=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_7zip=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_firefox=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_nodejs=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_powershell=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_r=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"has_git=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 " - $_" }
|
||||
|
||||
# Check if google-chrome/windows is in the changed apps
|
||||
# Use -in operator which works for both arrays and single values
|
||||
if ("google-chrome/windows" -in $windowsSlugs) {
|
||||
"has_google_chrome=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
Write-Host "Google Chrome detected in changed apps"
|
||||
} else {
|
||||
"has_google_chrome=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
# Check if 7-zip/windows is in the changed apps
|
||||
if ("7-zip/windows" -in $windowsSlugs) {
|
||||
"has_7zip=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
Write-Host "7-zip detected in changed apps"
|
||||
} else {
|
||||
"has_7zip=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
# Check if firefox/windows or firefox@esr/windows is in the changed apps
|
||||
if (("firefox/windows" -in $windowsSlugs) -or ("firefox@esr/windows" -in $windowsSlugs)) {
|
||||
"has_firefox=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
Write-Host "Firefox detected in changed apps"
|
||||
} else {
|
||||
"has_firefox=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
# Check if nodejs/windows is in the changed apps
|
||||
if ("nodejs/windows" -in $windowsSlugs) {
|
||||
"has_nodejs=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
Write-Host "Node.js detected in changed apps"
|
||||
} else {
|
||||
"has_nodejs=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
# Check if powershell/windows is in the changed apps
|
||||
if ("powershell/windows" -in $windowsSlugs) {
|
||||
"has_powershell=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
Write-Host "PowerShell detected in changed apps"
|
||||
} else {
|
||||
"has_powershell=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
# Check if r/windows is in the changed apps
|
||||
if ("r/windows" -in $windowsSlugs) {
|
||||
"has_r=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
Write-Host "R detected in changed apps"
|
||||
} else {
|
||||
"has_r=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
# Check if git/windows is in the changed apps
|
||||
if ("git/windows" -in $windowsSlugs) {
|
||||
"has_git=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
Write-Host "Git detected in changed apps"
|
||||
} else {
|
||||
"has_git=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Install osquery windows
|
||||
if: steps.check-windows-apps.outputs.has_windows_apps == 'true'
|
||||
run: |
|
||||
Write-Host "Runner architecture: $env:PROCESSOR_ARCHITECTURE"
|
||||
# Use the native osquery build for the runner architecture. On
|
||||
# windows-11-arm this picks the arm64 zip so osqueryi runs natively
|
||||
# rather than under Prism emulation; x86_64 runners keep the x64 zip.
|
||||
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
|
||||
$osqueryAsset = "osquery-5.18.1.windows_arm64.zip"
|
||||
} else {
|
||||
$osqueryAsset = "osquery-5.18.1.windows_x86_64.zip"
|
||||
}
|
||||
Write-Host "Downloading osquery asset: $osqueryAsset"
|
||||
curl -L -o osquery.zip "https://github.com/osquery/osquery/releases/download/5.18.1/$osqueryAsset"
|
||||
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' && steps.check-windows-apps.outputs.has_google_chrome == '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: Remove pre-installed 7-zip
|
||||
if: steps.check-windows-apps.outputs.has_windows_apps == 'true' && steps.check-windows-apps.outputs.has_7zip == 'true'
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing '7-Zip':"
|
||||
Get-Package | Where-Object { $_.Name -like "*7-Zip*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
# Check registry for 7-Zip uninstaller
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$uninstallEntry = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "*7-Zip*" -and $_.Publisher -like "*Igor Pavlov*" }
|
||||
if ($uninstallEntry) {
|
||||
$found = $true
|
||||
Write-Host "Found 7-Zip uninstall entry: $($uninstallEntry.DisplayName)"
|
||||
|
||||
# Try to get uninstall string
|
||||
$uninstallString = if ($uninstallEntry.QuietUninstallString) {
|
||||
$uninstallEntry.QuietUninstallString
|
||||
} elseif ($uninstallEntry.UninstallString) {
|
||||
$uninstallEntry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Found 7-Zip uninstall path: $uninstallString"
|
||||
try {
|
||||
# Check if it's an MSI uninstall (contains /X or /I)
|
||||
if ($uninstallString -match "/X\{([A-F0-9\-]+)\}") {
|
||||
$guid = $matches[1]
|
||||
Write-Host "Uninstalling 7-Zip MSI with GUID: $guid"
|
||||
Start-Process -FilePath "msiexec.exe" -ArgumentList "/X{$guid}", "/quiet", "/norestart" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed 7-Zip via MSI uninstaller"
|
||||
} elseif ($uninstallString -match '"([^"]+)"') {
|
||||
# Extract executable path
|
||||
$exePath = $matches[1]
|
||||
Write-Host "Uninstalling 7-Zip via executable: $exePath"
|
||||
# 7-Zip typically uses /S for silent uninstall
|
||||
Start-Process -FilePath $exePath -ArgumentList "/S" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed 7-Zip via executable uninstaller"
|
||||
} else {
|
||||
Write-Host "Could not parse uninstall string format: $uninstallString"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove 7-Zip: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "7-Zip uninstall string not found in registry entry"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "7-Zip uninstall path not found in registry"
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Remove pre-installed Firefox
|
||||
if: steps.check-windows-apps.outputs.has_windows_apps == 'true' && steps.check-windows-apps.outputs.has_firefox == 'true'
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing 'Firefox':"
|
||||
Get-Package | Where-Object { $_.Name -like "*Firefox*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "*Mozilla Firefox*" }
|
||||
foreach ($entry in $entries) {
|
||||
if (-not $entry) { continue }
|
||||
$found = $true
|
||||
Write-Host "Found Firefox: $($entry.DisplayName)"
|
||||
|
||||
$uninstallString = if ($entry.QuietUninstallString) {
|
||||
$entry.QuietUninstallString
|
||||
} elseif ($entry.UninstallString) {
|
||||
$entry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Uninstall string: $uninstallString"
|
||||
try {
|
||||
$splitArgs = $uninstallString.Split('"')
|
||||
if ($splitArgs.Length -ge 3) {
|
||||
$exePath = $splitArgs[1]
|
||||
Write-Host "Uninstalling Firefox via: $exePath /S"
|
||||
Start-Process -FilePath $exePath -ArgumentList "/S" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed $($entry.DisplayName)"
|
||||
} else {
|
||||
Write-Host "Uninstalling Firefox via: $uninstallString /S"
|
||||
Start-Process -FilePath $uninstallString -ArgumentList "/S" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed $($entry.DisplayName)"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove Firefox: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "Firefox uninstall string not found in registry entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "Firefox not found in registry"
|
||||
}
|
||||
|
||||
# Kill any lingering Firefox/Mozilla processes
|
||||
Write-Host "Stopping any lingering Firefox processes..."
|
||||
Get-Process -Name "firefox","plugin-container","updater","maintenanceservice*","helper" -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-Host " Killing process: $($_.Name) (PID: $($_.Id))"
|
||||
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 10
|
||||
|
||||
# Force-remove leftover Firefox directories from Program Files
|
||||
$firefoxDirs = @(
|
||||
"C:\Program Files\Mozilla Firefox",
|
||||
"C:\Program Files (x86)\Mozilla Firefox",
|
||||
"C:\Program Files\Mozilla Maintenance Service"
|
||||
)
|
||||
foreach ($dir in $firefoxDirs) {
|
||||
if (Test-Path $dir) {
|
||||
Write-Host "Removing leftover directory: $dir"
|
||||
Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $dir) {
|
||||
Write-Host "WARNING: Failed to fully remove $dir"
|
||||
} else {
|
||||
Write-Host "Removed $dir"
|
||||
}
|
||||
}
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Remove pre-installed Node.js
|
||||
if: steps.check-windows-apps.outputs.has_windows_apps == 'true' && steps.check-windows-apps.outputs.has_nodejs == 'true'
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing 'Node':"
|
||||
Get-Package | Where-Object { $_.Name -like "*Node*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
# Node.js installs via MSI and registers under "Node.js" / "Node.js Foundation".
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "Node.js*" -and $_.Publisher -like "*Node.js Foundation*" }
|
||||
foreach ($entry in $entries) {
|
||||
if (-not $entry) { continue }
|
||||
$found = $true
|
||||
Write-Host "Found Node.js uninstall entry: $($entry.DisplayName) (Version: $($entry.DisplayVersion))"
|
||||
|
||||
$uninstallString = if ($entry.QuietUninstallString) {
|
||||
$entry.QuietUninstallString
|
||||
} elseif ($entry.UninstallString) {
|
||||
$entry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Found Node.js uninstall path: $uninstallString"
|
||||
try {
|
||||
# Node.js uses an MSI uninstaller (MsiExec.exe /X{GUID} or /I{GUID})
|
||||
if ($uninstallString -match "/[XI]\{([A-F0-9\-]+)\}") {
|
||||
$guid = $matches[1]
|
||||
Write-Host "Uninstalling Node.js MSI with GUID: $guid"
|
||||
Start-Process -FilePath "msiexec.exe" -ArgumentList "/X{$guid}", "/quiet", "/norestart" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed Node.js via MSI uninstaller"
|
||||
} else {
|
||||
Write-Host "Could not parse uninstall string format: $uninstallString"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove Node.js: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "Node.js uninstall string not found in registry entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "Node.js uninstall path not found in registry"
|
||||
}
|
||||
|
||||
# Force-remove leftover Node.js directory in case files remain after MSI removal
|
||||
$nodeDir = "C:\Program Files\nodejs"
|
||||
if (Test-Path $nodeDir) {
|
||||
Write-Host "Removing leftover directory: $nodeDir"
|
||||
Remove-Item -Path $nodeDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $nodeDir) {
|
||||
Write-Host "WARNING: Failed to fully remove $nodeDir"
|
||||
} else {
|
||||
Write-Host "Removed $nodeDir"
|
||||
}
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Remove pre-installed PowerShell
|
||||
if: steps.check-windows-apps.outputs.has_windows_apps == 'true' && steps.check-windows-apps.outputs.has_powershell == 'true'
|
||||
# NOTE: this step (and the verify step below) run under Windows PowerShell 5.1
|
||||
# (shell: powershell), NOT pwsh. We are about to uninstall PowerShell 7, so we
|
||||
# must not be executing inside pwsh.exe (it would be locked / unavailable).
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing 'PowerShell':"
|
||||
Get-Package | Where-Object { $_.Name -like "*PowerShell*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
# PowerShell 7 installs via MSI and registers under "PowerShell 7-x64" /
|
||||
# "Microsoft Corporation". GitHub-hosted windows runners ship with it
|
||||
# pre-installed, which must be removed so the validator starts from a clean state.
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "PowerShell 7*" -and $_.Publisher -like "*Microsoft Corporation*" }
|
||||
foreach ($entry in $entries) {
|
||||
if (-not $entry) { continue }
|
||||
$found = $true
|
||||
Write-Host "Found PowerShell uninstall entry: $($entry.DisplayName) (Version: $($entry.DisplayVersion))"
|
||||
|
||||
$uninstallString = if ($entry.QuietUninstallString) {
|
||||
$entry.QuietUninstallString
|
||||
} elseif ($entry.UninstallString) {
|
||||
$entry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Found PowerShell uninstall path: $uninstallString"
|
||||
try {
|
||||
# PowerShell 7 uses an MSI uninstaller (MsiExec.exe /X{GUID} or /I{GUID})
|
||||
if ($uninstallString -match "/[XI]\{([A-F0-9\-]+)\}") {
|
||||
$guid = $matches[1]
|
||||
Write-Host "Uninstalling PowerShell MSI with GUID: $guid"
|
||||
Start-Process -FilePath "msiexec.exe" -ArgumentList "/X{$guid}", "/quiet", "/norestart" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed PowerShell via MSI uninstaller"
|
||||
} else {
|
||||
Write-Host "Could not parse uninstall string format: $uninstallString"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove PowerShell: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "PowerShell uninstall string not found in registry entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "PowerShell uninstall path not found in registry"
|
||||
}
|
||||
|
||||
# Force-remove leftover PowerShell 7 directory in case files remain after MSI removal
|
||||
$psDir = "C:\Program Files\PowerShell\7"
|
||||
if (Test-Path $psDir) {
|
||||
Write-Host "Removing leftover directory: $psDir"
|
||||
Remove-Item -Path $psDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $psDir) {
|
||||
Write-Host "WARNING: Failed to fully remove $psDir"
|
||||
} else {
|
||||
Write-Host "Removed $psDir"
|
||||
}
|
||||
}
|
||||
shell: powershell
|
||||
|
||||
- name: Remove pre-installed R
|
||||
if: steps.check-windows-apps.outputs.has_windows_apps == 'true' && steps.check-windows-apps.outputs.has_r == 'true'
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing 'R for Windows':"
|
||||
Get-Package | Where-Object { $_.Name -like "*R for Windows*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
# Stop any R processes so the uninstaller doesn't fail on locked files
|
||||
Get-Process -Name "Rgui","Rterm","Rscript" -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# R for Windows installs via Inno Setup and registers under "R for Windows <ver>"
|
||||
# / "R Core Team". The version is embedded in the DisplayName, so match by prefix
|
||||
# and use the registry UninstallString (Inno has no MSI ProductCode).
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "R for Windows*" -and $_.Publisher -like "*R Core Team*" }
|
||||
foreach ($entry in $entries) {
|
||||
if (-not $entry) { continue }
|
||||
$found = $true
|
||||
Write-Host "Found R uninstall entry: $($entry.DisplayName) (Version: $($entry.DisplayVersion))"
|
||||
|
||||
$uninstallString = if ($entry.QuietUninstallString) {
|
||||
$entry.QuietUninstallString
|
||||
} elseif ($entry.UninstallString) {
|
||||
$entry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Found R uninstall path: $uninstallString"
|
||||
try {
|
||||
# R uses an Inno Setup uninstaller (unins000.exe). Parse the exe path
|
||||
# (quoted or unquoted) and run it with silent Inno switches.
|
||||
$exePath = ""
|
||||
if ($uninstallString -match '^\s*"([^"]+)"') {
|
||||
$exePath = $matches[1]
|
||||
} elseif ($uninstallString -match '(?i)^\s*(.+?\.exe)') {
|
||||
$exePath = $matches[1]
|
||||
}
|
||||
if ($exePath) {
|
||||
Write-Host "Uninstalling R via: $exePath"
|
||||
Start-Process -FilePath $exePath -ArgumentList "/VERYSILENT","/SUPPRESSMSGBOXES","/NORESTART" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed R via Inno uninstaller"
|
||||
} else {
|
||||
Write-Host "Could not parse uninstall string format: $uninstallString"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove R: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "R uninstall string not found in registry entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "R uninstall path not found in registry"
|
||||
}
|
||||
|
||||
# Force-remove leftover R directory in case files remain after uninstall
|
||||
$rDir = "C:\Program Files\R"
|
||||
if (Test-Path $rDir) {
|
||||
Write-Host "Removing leftover directory: $rDir"
|
||||
Remove-Item -Path $rDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $rDir) {
|
||||
Write-Host "WARNING: Failed to fully remove $rDir"
|
||||
} else {
|
||||
Write-Host "Removed $rDir"
|
||||
}
|
||||
}
|
||||
# Use Windows PowerShell 5.1 (not pwsh): the "Remove pre-installed PowerShell"
|
||||
# step above may have uninstalled PowerShell 7, so pwsh.exe may be unavailable.
|
||||
shell: powershell
|
||||
|
||||
# NOTE: filtering is split out from validation and runs BEFORE "Remove pre-installed
|
||||
# Git" below. Git for Windows provides the Git Bash 'bash' that this step's
|
||||
# filter-apps-json.sh call depends on; validation itself does not need bash.
|
||||
- name: Filter apps.json for 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" })
|
||||
|
||||
# Build a JSON array of the windows slugs. Construct it explicitly so a single
|
||||
# slug still serializes as an array (Windows PowerShell unwraps single-element
|
||||
# arrays via ConvertTo-Json), and write it to a BOM-free file. We then pass the
|
||||
# file PATH -- not the JSON string -- to the bash script: forwarding a quoted
|
||||
# JSON string across the PowerShell -> bash argument boundary mangles the
|
||||
# embedded quotes under Windows PowerShell 5.1, which breaks jq --argjson.
|
||||
$slugsArray = @($windowsSlugs)
|
||||
$windowsSlugsJson = "[" + (($slugsArray | ForEach-Object { $_ | ConvertTo-Json -Compress }) -join ",") + "]"
|
||||
Write-Host "Filtering apps.json for slugs: $windowsSlugsJson"
|
||||
|
||||
$windowsSlugsFile = Join-Path $env:TEMP "windows-slugs-$(New-Guid).json"
|
||||
Set-Content -Path $windowsSlugsFile -Value $windowsSlugsJson -Encoding ascii -NoNewline
|
||||
# Use forward slashes so Git Bash reads the path reliably (it reads this arg as a file).
|
||||
$windowsSlugsFileForBash = $windowsSlugsFile -replace '\\', '/'
|
||||
|
||||
# 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 "$windowsSlugsFileForBash" "$filteredAppsJson"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Error: filter-apps-json.sh failed with exit code $LASTEXITCODE"
|
||||
echo "detect-changed-apps: $DETECT_RESULT"
|
||||
echo "validate: $VALIDATE_RESULT"
|
||||
if [ "$DETECT_RESULT" != "success" ]; then
|
||||
echo "Detecting changed apps failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Verify the filtered file was created
|
||||
if (-not (Test-Path $filteredAppsJson)) {
|
||||
Write-Host "Error: Filtered apps.json was not created at $filteredAppsJson"
|
||||
fi
|
||||
# validate is skipped when the PR changes no Windows apps.
|
||||
if [ "$VALIDATE_RESULT" != "success" ] && [ "$VALIDATE_RESULT" != "skipped" ]; then
|
||||
echo "Validation failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Replace apps.json with filtered version
|
||||
Move-Item -Path $filteredAppsJson -Destination "ee\maintained-apps\outputs\apps.json" -Force
|
||||
# Use Windows PowerShell 5.1 (not pwsh): the "Remove pre-installed PowerShell"
|
||||
# step above may have uninstalled PowerShell 7, so pwsh.exe may be unavailable.
|
||||
shell: powershell
|
||||
|
||||
- name: Remove pre-installed Git
|
||||
if: steps.check-windows-apps.outputs.has_windows_apps == 'true' && steps.check-windows-apps.outputs.has_git == 'true'
|
||||
# IMPORTANT: this MUST run AFTER "Filter apps.json for changed apps" (which uses Git
|
||||
# Bash) and BEFORE "Validate changed apps". Git for Windows provides the 'bash' the
|
||||
# filter step relies on; validation runs 'go run -buildvcs=false' and needs no bash.
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing 'Git':"
|
||||
Get-Package | Where-Object { $_.Name -like "*Git*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
# Stop Git-related processes so the uninstaller doesn't fail on locked files
|
||||
Get-Process -Name "git","bash","sh","ssh-agent","gitk","wish" -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Git for Windows installs via Inno Setup. Its registry DisplayName is not
|
||||
# reliably "Git version <ver>" (the runner's pre-installed Git is listed as
|
||||
# just "Git"), so anchor on the publisher -- which is unique to Git for
|
||||
# Windows -- and loosely guard the DisplayName. Use the registry
|
||||
# UninstallString (Inno has no MSI ProductCode).
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "Git*" -and $_.Publisher -like "*The Git Development Community*" }
|
||||
foreach ($entry in $entries) {
|
||||
if (-not $entry) { continue }
|
||||
$found = $true
|
||||
Write-Host "Found Git uninstall entry: $($entry.DisplayName) (Version: $($entry.DisplayVersion))"
|
||||
|
||||
$uninstallString = if ($entry.QuietUninstallString) {
|
||||
$entry.QuietUninstallString
|
||||
} elseif ($entry.UninstallString) {
|
||||
$entry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Found Git uninstall path: $uninstallString"
|
||||
try {
|
||||
# Git for Windows uses an Inno Setup uninstaller (unins000.exe). Parse the
|
||||
# exe path (quoted or unquoted) and run it with silent Inno switches.
|
||||
$exePath = ""
|
||||
if ($uninstallString -match '^\s*"([^"]+)"') {
|
||||
$exePath = $matches[1]
|
||||
} elseif ($uninstallString -match '(?i)^\s*(.+?\.exe)') {
|
||||
$exePath = $matches[1]
|
||||
}
|
||||
if ($exePath) {
|
||||
Write-Host "Uninstalling Git via: $exePath"
|
||||
Start-Process -FilePath $exePath -ArgumentList "/VERYSILENT","/SUPPRESSMSGBOXES","/NORESTART" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed Git via Inno uninstaller"
|
||||
} else {
|
||||
Write-Host "Could not parse uninstall string format: $uninstallString"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove Git: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "Git uninstall string not found in registry entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "Git uninstall path not found in registry"
|
||||
}
|
||||
|
||||
# Force-remove leftover Git directory in case files remain after uninstall
|
||||
$gitDir = "C:\Program Files\Git"
|
||||
if (Test-Path $gitDir) {
|
||||
Write-Host "Removing leftover directory: $gitDir"
|
||||
Remove-Item -Path $gitDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $gitDir) {
|
||||
Write-Host "WARNING: Failed to fully remove $gitDir"
|
||||
} else {
|
||||
Write-Host "Removed $gitDir"
|
||||
}
|
||||
}
|
||||
# Use Windows PowerShell 5.1 (not pwsh): the "Remove pre-installed PowerShell"
|
||||
# step above may have uninstalled PowerShell 7, so pwsh.exe may be unavailable.
|
||||
shell: powershell
|
||||
|
||||
- name: Validate changed apps
|
||||
if: steps.check-windows-apps.outputs.has_windows_apps == 'true'
|
||||
# -buildvcs=false so 'go run' does not invoke git for VCS stamping: the
|
||||
# "Remove pre-installed Git" step above may have removed git from the runner.
|
||||
run: |
|
||||
cd fleet
|
||||
$env:GITHUB_WORKSPACE = (Get-Location).Path
|
||||
|
||||
# Run validation
|
||||
ls "C:\Program Files"
|
||||
go run -buildvcs=false ./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
|
||||
# Use Windows PowerShell 5.1 (not pwsh): when validating the PowerShell FMA we
|
||||
# uninstall PowerShell 7 in the step above, so pwsh.exe may not be available here.
|
||||
shell: powershell
|
||||
fi
|
||||
echo "All Windows FMA validations passed"
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
# Reusable workflow that installs and validates a set of Windows Fleet-maintained
|
||||
# apps on a runner whose native architecture matches the apps' installer
|
||||
# architecture (arm64 apps on windows-11-arm, x64/x86/neutral apps on the x64
|
||||
# runner). Called by test-fma-windows-pr-only.yml and test-fma-windows.yml with
|
||||
# a matrix produced by .github/scripts/partition-fma-apps.sh.
|
||||
name: Validate Fleet Maintained Apps - Windows
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
runner:
|
||||
description: 'Runner label matching the apps'' installer architecture (e.g. "windows-latest" for x64/x86, "windows-11-arm" for arm64)'
|
||||
required: true
|
||||
type: string
|
||||
slugs:
|
||||
description: 'JSON array of app slugs to validate (e.g. ["7-zip/windows"])'
|
||||
required: true
|
||||
type: string
|
||||
log_level:
|
||||
description: "Log level (debug, info, warn, error)"
|
||||
required: false
|
||||
type: string
|
||||
default: "info"
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LOG_LEVEL: ${{ inputs.log_level }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ${{ inputs.runner }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
# Changed-app detection and architecture partitioning happen in the
|
||||
# calling workflow on a Linux runner, so no git history is needed here.
|
||||
- name: Checkout Fleet
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
path: fleet
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version-file: "fleet/go.mod"
|
||||
|
||||
- name: Determine pre-installed apps to remove
|
||||
id: check-windows-apps
|
||||
# Pass the slugs through env rather than expanding ${{ inputs.slugs }}
|
||||
# into the script body (flagged by zizmor as template injection).
|
||||
env:
|
||||
SLUGS_JSON: ${{ inputs.slugs }}
|
||||
run: |
|
||||
# SLUGS_JSON is a JSON array; wrap in @() so a single slug still
|
||||
# behaves as an array.
|
||||
$slugs = @($env:SLUGS_JSON | ConvertFrom-Json)
|
||||
Write-Host "Apps to validate on this $env:PROCESSOR_ARCHITECTURE runner:"
|
||||
$slugs | ForEach-Object { Write-Host " - $_" }
|
||||
|
||||
# The runner images ship with some of the apps we validate already
|
||||
# installed; flag the ones present in this shard so the removal steps
|
||||
# below start the validator from a clean state.
|
||||
$flags = [ordered]@{
|
||||
has_google_chrome = ("google-chrome/windows" -in $slugs)
|
||||
has_7zip = ("7-zip/windows" -in $slugs)
|
||||
has_firefox = (("firefox/windows" -in $slugs) -or ("firefox@esr/windows" -in $slugs))
|
||||
has_nodejs = ("nodejs/windows" -in $slugs)
|
||||
has_powershell = ("powershell/windows" -in $slugs)
|
||||
has_r = ("r/windows" -in $slugs)
|
||||
has_git = ("git/windows" -in $slugs)
|
||||
}
|
||||
foreach ($key in $flags.Keys) {
|
||||
"$key=$($flags[$key].ToString().ToLower())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
if ($flags[$key]) { Write-Host "$key detected in this shard" }
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Install osquery windows
|
||||
run: |
|
||||
Write-Host "Runner architecture: $env:PROCESSOR_ARCHITECTURE"
|
||||
# Use the native osquery build for the runner architecture. On
|
||||
# windows-11-arm this picks the arm64 zip so osqueryi runs natively
|
||||
# rather than under Prism emulation; x86_64 runners keep the x64 zip.
|
||||
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
|
||||
$osqueryAsset = "osquery-5.18.1.windows_arm64.zip"
|
||||
} else {
|
||||
$osqueryAsset = "osquery-5.18.1.windows_x86_64.zip"
|
||||
}
|
||||
Write-Host "Downloading osquery asset: $osqueryAsset"
|
||||
curl -L -o osquery.zip "https://github.com/osquery/osquery/releases/download/5.18.1/$osqueryAsset"
|
||||
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_google_chrome == '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: Remove pre-installed 7-zip
|
||||
if: steps.check-windows-apps.outputs.has_7zip == 'true'
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing '7-Zip':"
|
||||
Get-Package | Where-Object { $_.Name -like "*7-Zip*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
# Check registry for 7-Zip uninstaller
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$uninstallEntry = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "*7-Zip*" -and $_.Publisher -like "*Igor Pavlov*" }
|
||||
if ($uninstallEntry) {
|
||||
$found = $true
|
||||
Write-Host "Found 7-Zip uninstall entry: $($uninstallEntry.DisplayName)"
|
||||
|
||||
# Try to get uninstall string
|
||||
$uninstallString = if ($uninstallEntry.QuietUninstallString) {
|
||||
$uninstallEntry.QuietUninstallString
|
||||
} elseif ($uninstallEntry.UninstallString) {
|
||||
$uninstallEntry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Found 7-Zip uninstall path: $uninstallString"
|
||||
try {
|
||||
# Check if it's an MSI uninstall (contains /X or /I)
|
||||
if ($uninstallString -match "/X\{([A-F0-9\-]+)\}") {
|
||||
$guid = $matches[1]
|
||||
Write-Host "Uninstalling 7-Zip MSI with GUID: $guid"
|
||||
Start-Process -FilePath "msiexec.exe" -ArgumentList "/X{$guid}", "/quiet", "/norestart" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed 7-Zip via MSI uninstaller"
|
||||
} elseif ($uninstallString -match '"([^"]+)"') {
|
||||
# Extract executable path
|
||||
$exePath = $matches[1]
|
||||
Write-Host "Uninstalling 7-Zip via executable: $exePath"
|
||||
# 7-Zip typically uses /S for silent uninstall
|
||||
Start-Process -FilePath $exePath -ArgumentList "/S" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed 7-Zip via executable uninstaller"
|
||||
} else {
|
||||
Write-Host "Could not parse uninstall string format: $uninstallString"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove 7-Zip: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "7-Zip uninstall string not found in registry entry"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "7-Zip uninstall path not found in registry"
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Remove pre-installed Firefox
|
||||
if: steps.check-windows-apps.outputs.has_firefox == 'true'
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing 'Firefox':"
|
||||
Get-Package | Where-Object { $_.Name -like "*Firefox*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "*Mozilla Firefox*" }
|
||||
foreach ($entry in $entries) {
|
||||
if (-not $entry) { continue }
|
||||
$found = $true
|
||||
Write-Host "Found Firefox: $($entry.DisplayName)"
|
||||
|
||||
$uninstallString = if ($entry.QuietUninstallString) {
|
||||
$entry.QuietUninstallString
|
||||
} elseif ($entry.UninstallString) {
|
||||
$entry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Uninstall string: $uninstallString"
|
||||
try {
|
||||
$splitArgs = $uninstallString.Split('"')
|
||||
if ($splitArgs.Length -ge 3) {
|
||||
$exePath = $splitArgs[1]
|
||||
Write-Host "Uninstalling Firefox via: $exePath /S"
|
||||
Start-Process -FilePath $exePath -ArgumentList "/S" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed $($entry.DisplayName)"
|
||||
} else {
|
||||
Write-Host "Uninstalling Firefox via: $uninstallString /S"
|
||||
Start-Process -FilePath $uninstallString -ArgumentList "/S" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed $($entry.DisplayName)"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove Firefox: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "Firefox uninstall string not found in registry entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "Firefox not found in registry"
|
||||
}
|
||||
|
||||
# Kill any lingering Firefox/Mozilla processes
|
||||
Write-Host "Stopping any lingering Firefox processes..."
|
||||
Get-Process -Name "firefox","plugin-container","updater","maintenanceservice*","helper" -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-Host " Killing process: $($_.Name) (PID: $($_.Id))"
|
||||
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 10
|
||||
|
||||
# Force-remove leftover Firefox directories from Program Files
|
||||
$firefoxDirs = @(
|
||||
"C:\Program Files\Mozilla Firefox",
|
||||
"C:\Program Files (x86)\Mozilla Firefox",
|
||||
"C:\Program Files\Mozilla Maintenance Service"
|
||||
)
|
||||
foreach ($dir in $firefoxDirs) {
|
||||
if (Test-Path $dir) {
|
||||
Write-Host "Removing leftover directory: $dir"
|
||||
Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $dir) {
|
||||
Write-Host "WARNING: Failed to fully remove $dir"
|
||||
} else {
|
||||
Write-Host "Removed $dir"
|
||||
}
|
||||
}
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Remove pre-installed Node.js
|
||||
if: steps.check-windows-apps.outputs.has_nodejs == 'true'
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing 'Node':"
|
||||
Get-Package | Where-Object { $_.Name -like "*Node*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
# Node.js installs via MSI and registers under "Node.js" / "Node.js Foundation".
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "Node.js*" -and $_.Publisher -like "*Node.js Foundation*" }
|
||||
foreach ($entry in $entries) {
|
||||
if (-not $entry) { continue }
|
||||
$found = $true
|
||||
Write-Host "Found Node.js uninstall entry: $($entry.DisplayName) (Version: $($entry.DisplayVersion))"
|
||||
|
||||
$uninstallString = if ($entry.QuietUninstallString) {
|
||||
$entry.QuietUninstallString
|
||||
} elseif ($entry.UninstallString) {
|
||||
$entry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Found Node.js uninstall path: $uninstallString"
|
||||
try {
|
||||
# Node.js uses an MSI uninstaller (MsiExec.exe /X{GUID} or /I{GUID})
|
||||
if ($uninstallString -match "/[XI]\{([A-F0-9\-]+)\}") {
|
||||
$guid = $matches[1]
|
||||
Write-Host "Uninstalling Node.js MSI with GUID: $guid"
|
||||
Start-Process -FilePath "msiexec.exe" -ArgumentList "/X{$guid}", "/quiet", "/norestart" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed Node.js via MSI uninstaller"
|
||||
} else {
|
||||
Write-Host "Could not parse uninstall string format: $uninstallString"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove Node.js: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "Node.js uninstall string not found in registry entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "Node.js uninstall path not found in registry"
|
||||
}
|
||||
|
||||
# Force-remove leftover Node.js directory in case files remain after MSI removal
|
||||
$nodeDir = "C:\Program Files\nodejs"
|
||||
if (Test-Path $nodeDir) {
|
||||
Write-Host "Removing leftover directory: $nodeDir"
|
||||
Remove-Item -Path $nodeDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $nodeDir) {
|
||||
Write-Host "WARNING: Failed to fully remove $nodeDir"
|
||||
} else {
|
||||
Write-Host "Removed $nodeDir"
|
||||
}
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Remove pre-installed PowerShell
|
||||
if: steps.check-windows-apps.outputs.has_powershell == 'true'
|
||||
# NOTE: this step (and the steps below) run under Windows PowerShell 5.1
|
||||
# (shell: powershell), NOT pwsh. We are about to uninstall PowerShell 7, so we
|
||||
# must not be executing inside pwsh.exe (it would be locked / unavailable).
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing 'PowerShell':"
|
||||
Get-Package | Where-Object { $_.Name -like "*PowerShell*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
# PowerShell 7 installs via MSI and registers under "PowerShell 7-x64" /
|
||||
# "Microsoft Corporation". GitHub-hosted windows runners ship with it
|
||||
# pre-installed, which must be removed so the validator starts from a clean state.
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "PowerShell 7*" -and $_.Publisher -like "*Microsoft Corporation*" }
|
||||
foreach ($entry in $entries) {
|
||||
if (-not $entry) { continue }
|
||||
$found = $true
|
||||
Write-Host "Found PowerShell uninstall entry: $($entry.DisplayName) (Version: $($entry.DisplayVersion))"
|
||||
|
||||
$uninstallString = if ($entry.QuietUninstallString) {
|
||||
$entry.QuietUninstallString
|
||||
} elseif ($entry.UninstallString) {
|
||||
$entry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Found PowerShell uninstall path: $uninstallString"
|
||||
try {
|
||||
# PowerShell 7 uses an MSI uninstaller (MsiExec.exe /X{GUID} or /I{GUID})
|
||||
if ($uninstallString -match "/[XI]\{([A-F0-9\-]+)\}") {
|
||||
$guid = $matches[1]
|
||||
Write-Host "Uninstalling PowerShell MSI with GUID: $guid"
|
||||
Start-Process -FilePath "msiexec.exe" -ArgumentList "/X{$guid}", "/quiet", "/norestart" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed PowerShell via MSI uninstaller"
|
||||
} else {
|
||||
Write-Host "Could not parse uninstall string format: $uninstallString"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove PowerShell: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "PowerShell uninstall string not found in registry entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "PowerShell uninstall path not found in registry"
|
||||
}
|
||||
|
||||
# Force-remove leftover PowerShell 7 directory in case files remain after MSI removal
|
||||
$psDir = "C:\Program Files\PowerShell\7"
|
||||
if (Test-Path $psDir) {
|
||||
Write-Host "Removing leftover directory: $psDir"
|
||||
Remove-Item -Path $psDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $psDir) {
|
||||
Write-Host "WARNING: Failed to fully remove $psDir"
|
||||
} else {
|
||||
Write-Host "Removed $psDir"
|
||||
}
|
||||
}
|
||||
shell: powershell
|
||||
|
||||
- name: Remove pre-installed R
|
||||
if: steps.check-windows-apps.outputs.has_r == 'true'
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing 'R for Windows':"
|
||||
Get-Package | Where-Object { $_.Name -like "*R for Windows*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
# Stop any R processes so the uninstaller doesn't fail on locked files
|
||||
Get-Process -Name "Rgui","Rterm","Rscript" -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# R for Windows installs via Inno Setup and registers under "R for Windows <ver>"
|
||||
# / "R Core Team". The version is embedded in the DisplayName, so match by prefix
|
||||
# and use the registry UninstallString (Inno has no MSI ProductCode).
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "R for Windows*" -and $_.Publisher -like "*R Core Team*" }
|
||||
foreach ($entry in $entries) {
|
||||
if (-not $entry) { continue }
|
||||
$found = $true
|
||||
Write-Host "Found R uninstall entry: $($entry.DisplayName) (Version: $($entry.DisplayVersion))"
|
||||
|
||||
$uninstallString = if ($entry.QuietUninstallString) {
|
||||
$entry.QuietUninstallString
|
||||
} elseif ($entry.UninstallString) {
|
||||
$entry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Found R uninstall path: $uninstallString"
|
||||
try {
|
||||
# R uses an Inno Setup uninstaller (unins000.exe). Parse the exe path
|
||||
# (quoted or unquoted) and run it with silent Inno switches.
|
||||
$exePath = ""
|
||||
if ($uninstallString -match '^\s*"([^"]+)"') {
|
||||
$exePath = $matches[1]
|
||||
} elseif ($uninstallString -match '(?i)^\s*(.+?\.exe)') {
|
||||
$exePath = $matches[1]
|
||||
}
|
||||
if ($exePath) {
|
||||
Write-Host "Uninstalling R via: $exePath"
|
||||
Start-Process -FilePath $exePath -ArgumentList "/VERYSILENT","/SUPPRESSMSGBOXES","/NORESTART" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed R via Inno uninstaller"
|
||||
} else {
|
||||
Write-Host "Could not parse uninstall string format: $uninstallString"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove R: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "R uninstall string not found in registry entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "R uninstall path not found in registry"
|
||||
}
|
||||
|
||||
# Force-remove leftover R directory in case files remain after uninstall
|
||||
$rDir = "C:\Program Files\R"
|
||||
if (Test-Path $rDir) {
|
||||
Write-Host "Removing leftover directory: $rDir"
|
||||
Remove-Item -Path $rDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $rDir) {
|
||||
Write-Host "WARNING: Failed to fully remove $rDir"
|
||||
} else {
|
||||
Write-Host "Removed $rDir"
|
||||
}
|
||||
}
|
||||
# Use Windows PowerShell 5.1 (not pwsh): the "Remove pre-installed PowerShell"
|
||||
# step above may have uninstalled PowerShell 7, so pwsh.exe may be unavailable.
|
||||
shell: powershell
|
||||
|
||||
# NOTE: filtering is split out from validation and runs BEFORE "Remove pre-installed
|
||||
# Git" below. Git for Windows provides the Git Bash 'bash' that this step's
|
||||
# filter-apps-json.sh call depends on; validation itself does not need bash.
|
||||
- name: Filter apps.json to this shard's apps
|
||||
# Pass the slugs through env rather than expanding ${{ inputs.slugs }}
|
||||
# into the script body (flagged by zizmor as template injection).
|
||||
env:
|
||||
SLUGS_JSON: ${{ inputs.slugs }}
|
||||
run: |
|
||||
cd fleet
|
||||
# Set GITHUB_WORKSPACE to current directory so scripts can find files
|
||||
$env:GITHUB_WORKSPACE = (Get-Location).Path
|
||||
|
||||
# The shard's slugs arrive as a compact JSON array string built by the
|
||||
# partition script, so no re-serialization is needed. Write it to a
|
||||
# BOM-free file and pass the file PATH -- not the JSON string -- to the
|
||||
# bash script: forwarding a quoted JSON string across the
|
||||
# PowerShell -> bash argument boundary mangles the embedded quotes under
|
||||
# Windows PowerShell 5.1, which breaks jq --argjson.
|
||||
$windowsSlugsJson = $env:SLUGS_JSON
|
||||
Write-Host "Filtering apps.json for slugs: $windowsSlugsJson"
|
||||
|
||||
$windowsSlugsFile = Join-Path $env:TEMP "windows-slugs-$(New-Guid).json"
|
||||
Set-Content -Path $windowsSlugsFile -Value $windowsSlugsJson -Encoding ascii -NoNewline
|
||||
# Use forward slashes so Git Bash reads the path reliably (it reads this arg as a file).
|
||||
$windowsSlugsFileForBash = $windowsSlugsFile -replace '\\', '/'
|
||||
|
||||
# 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 "$windowsSlugsFileForBash" "$filteredAppsJson"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Error: filter-apps-json.sh failed with exit code $LASTEXITCODE"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 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
|
||||
# Use Windows PowerShell 5.1 (not pwsh): the "Remove pre-installed PowerShell"
|
||||
# step above may have uninstalled PowerShell 7, so pwsh.exe may be unavailable.
|
||||
shell: powershell
|
||||
|
||||
- name: Remove pre-installed Git
|
||||
if: steps.check-windows-apps.outputs.has_git == 'true'
|
||||
# IMPORTANT: this MUST run AFTER "Filter apps.json to this shard's apps" (which uses
|
||||
# Git Bash) and BEFORE "Validate apps". Git for Windows provides the 'bash' the
|
||||
# filter step relies on; validation runs 'go run -buildvcs=false' and needs no bash.
|
||||
run: |
|
||||
Write-Host "Listing all installed packages containing 'Git':"
|
||||
Get-Package | Where-Object { $_.Name -like "*Git*" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name) (Version: $($_.Version))"
|
||||
}
|
||||
|
||||
# Stop Git-related processes so the uninstaller doesn't fail on locked files
|
||||
Get-Process -Name "git","bash","sh","ssh-agent","gitk","wish" -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Git for Windows installs via Inno Setup. Its registry DisplayName is not
|
||||
# reliably "Git version <ver>" (the runner's pre-installed Git is listed as
|
||||
# just "Git"), so anchor on the publisher -- which is unique to Git for
|
||||
# Windows -- and loosely guard the DisplayName. Use the registry
|
||||
# UninstallString (Inno has no MSI ProductCode).
|
||||
$uninstallPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
$found = $false
|
||||
foreach ($path in $uninstallPaths) {
|
||||
$entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "Git*" -and $_.Publisher -like "*The Git Development Community*" }
|
||||
foreach ($entry in $entries) {
|
||||
if (-not $entry) { continue }
|
||||
$found = $true
|
||||
Write-Host "Found Git uninstall entry: $($entry.DisplayName) (Version: $($entry.DisplayVersion))"
|
||||
|
||||
$uninstallString = if ($entry.QuietUninstallString) {
|
||||
$entry.QuietUninstallString
|
||||
} elseif ($entry.UninstallString) {
|
||||
$entry.UninstallString
|
||||
} else {
|
||||
$null
|
||||
}
|
||||
|
||||
if ($uninstallString) {
|
||||
Write-Host "Found Git uninstall path: $uninstallString"
|
||||
try {
|
||||
# Git for Windows uses an Inno Setup uninstaller (unins000.exe). Parse the
|
||||
# exe path (quoted or unquoted) and run it with silent Inno switches.
|
||||
$exePath = ""
|
||||
if ($uninstallString -match '^\s*"([^"]+)"') {
|
||||
$exePath = $matches[1]
|
||||
} elseif ($uninstallString -match '(?i)^\s*(.+?\.exe)') {
|
||||
$exePath = $matches[1]
|
||||
}
|
||||
if ($exePath) {
|
||||
Write-Host "Uninstalling Git via: $exePath"
|
||||
Start-Process -FilePath $exePath -ArgumentList "/VERYSILENT","/SUPPRESSMSGBOXES","/NORESTART" -Wait -NoNewWindow
|
||||
Write-Host "Successfully removed Git via Inno uninstaller"
|
||||
} else {
|
||||
Write-Host "Could not parse uninstall string format: $uninstallString"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Failed to remove Git: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "Git uninstall string not found in registry entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "Git uninstall path not found in registry"
|
||||
}
|
||||
|
||||
# Force-remove leftover Git directory in case files remain after uninstall
|
||||
$gitDir = "C:\Program Files\Git"
|
||||
if (Test-Path $gitDir) {
|
||||
Write-Host "Removing leftover directory: $gitDir"
|
||||
Remove-Item -Path $gitDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $gitDir) {
|
||||
Write-Host "WARNING: Failed to fully remove $gitDir"
|
||||
} else {
|
||||
Write-Host "Removed $gitDir"
|
||||
}
|
||||
}
|
||||
# Use Windows PowerShell 5.1 (not pwsh): the "Remove pre-installed PowerShell"
|
||||
# step above may have uninstalled PowerShell 7, so pwsh.exe may be unavailable.
|
||||
shell: powershell
|
||||
|
||||
- name: Validate apps
|
||||
# -buildvcs=false so 'go run' does not invoke git for VCS stamping: the
|
||||
# "Remove pre-installed Git" step above may have removed git from the runner.
|
||||
run: |
|
||||
cd fleet
|
||||
$env:GITHUB_WORKSPACE = (Get-Location).Path
|
||||
|
||||
# Run validation
|
||||
ls "C:\Program Files"
|
||||
go run -buildvcs=false ./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
|
||||
# Use Windows PowerShell 5.1 (not pwsh): when validating the PowerShell FMA we
|
||||
# uninstall PowerShell 7 in the step above, so pwsh.exe may not be available here.
|
||||
shell: powershell
|
||||
@@ -15,21 +15,26 @@ on:
|
||||
- info
|
||||
- warn
|
||||
- error
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shard_size:
|
||||
description: "Maximum number of apps per validation job (shards run in parallel)"
|
||||
required: false
|
||||
default: "20"
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-fma:
|
||||
env:
|
||||
LOG_LEVEL: ${{ github.event.inputs.log_level || 'info' }}
|
||||
# ARM64 runner so we can validate ARM-native FMAs. x86/x64 FMAs continue to
|
||||
# install and run here via Windows 11 on ARM's Prism emulation.
|
||||
runs-on: windows-11-arm
|
||||
|
||||
# Partition every Windows app in apps.json by installer architecture on a
|
||||
# cheap Linux runner, then fan out to Windows runners whose native
|
||||
# architecture matches each app's installer: arm64 apps to windows-11-arm,
|
||||
# x64/x86/neutral apps to the x64 runner. Each architecture bucket is split
|
||||
# into shards that validate in parallel.
|
||||
partition:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has_windows_apps: ${{ steps.partition.outputs.has_apps }}
|
||||
matrix: ${{ steps.partition.outputs.matrix }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
@@ -39,63 +44,59 @@ jobs:
|
||||
- name: Checkout Fleet
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: fleetdm/fleet
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.ref }}
|
||||
path: fleet
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
- name: Partition all Windows apps by architecture
|
||||
id: partition
|
||||
env:
|
||||
SHARD_SIZE: ${{ github.event.inputs.shard_size || '20' }}
|
||||
run: |
|
||||
ALL_WINDOWS_SLUGS=$(jq -c '[.apps[].slug | select(endswith("/windows"))]' ee/maintained-apps/outputs/apps.json)
|
||||
bash .github/scripts/partition-fma-apps.sh windows "$ALL_WINDOWS_SLUGS" "$SHARD_SIZE"
|
||||
|
||||
validate:
|
||||
needs: partition
|
||||
if: needs.partition.outputs.has_windows_apps == 'true'
|
||||
name: ${{ matrix.name }}
|
||||
strategy:
|
||||
# Keep validating the remaining shards even if one fails so a full run
|
||||
# reports every broken app, not just the first.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.partition.outputs.matrix) }}
|
||||
uses: ./.github/workflows/test-fma-windows-validate.yml
|
||||
permissions:
|
||||
contents: read
|
||||
with:
|
||||
runner: ${{ matrix.runner }}
|
||||
slugs: ${{ matrix.slugs }}
|
||||
log_level: ${{ github.event.inputs.log_level || 'info' }}
|
||||
|
||||
# Stable-named summary job aggregating the per-architecture/per-shard results.
|
||||
test-fma:
|
||||
needs: [partition, validate]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
||||
with:
|
||||
go-version-file: "fleet/go.mod"
|
||||
egress-policy: audit
|
||||
|
||||
- name: Install osquery windows
|
||||
- name: Check validation results
|
||||
env:
|
||||
PARTITION_RESULT: ${{ needs.partition.result }}
|
||||
VALIDATE_RESULT: ${{ needs.validate.result }}
|
||||
run: |
|
||||
Write-Host "Runner architecture: $env:PROCESSOR_ARCHITECTURE"
|
||||
# Use the native osquery build for the runner architecture. On
|
||||
# windows-11-arm this picks the arm64 zip so osqueryi runs natively
|
||||
# rather than under Prism emulation; x86_64 runners keep the x64 zip.
|
||||
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
|
||||
$osqueryAsset = "osquery-5.18.1.windows_arm64.zip"
|
||||
} else {
|
||||
$osqueryAsset = "osquery-5.18.1.windows_x86_64.zip"
|
||||
}
|
||||
Write-Host "Downloading osquery asset: $osqueryAsset"
|
||||
curl -L -o osquery.zip "https://github.com/osquery/osquery/releases/download/5.18.1/$osqueryAsset"
|
||||
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
|
||||
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: Verify Fleet Maintained Apps windows
|
||||
run: |
|
||||
ls "C:\Program Files"
|
||||
cd fleet
|
||||
go run ./cmd/maintained-apps/validate
|
||||
shell: pwsh
|
||||
echo "partition: $PARTITION_RESULT"
|
||||
echo "validate: $VALIDATE_RESULT"
|
||||
if [ "$PARTITION_RESULT" != "success" ]; then
|
||||
echo "Partitioning apps failed"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$VALIDATE_RESULT" != "success" ] && [ "$VALIDATE_RESULT" != "skipped" ]; then
|
||||
echo "Validation failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "All Windows FMA validations passed"
|
||||
|
||||
Reference in New Issue
Block a user