Improve changed app detection in CI (#37838)

This pull request improves the robustness and reliability of the script
and workflows that detect changed or new maintained apps in pull
requests. The main focus is on making the detection script pass
validation when the test is triggered but no new FMAs are detected.

**Script robustness and error handling:**

* The `.github/scripts/detect-new-fmas-in-pr.sh` script is updated to
always exit successfully (status 0) when no changes are detected, and
only exit with error (status 1) for critical failures like missing `jq`.
A new `safe_exit` function is introduced to standardize output and
ensure graceful exits.
[[1]](diffhunk://#diff-f9bbb0340f504713c99d610f3c64bf281fc13ed3cb8a1c06a5366272c9828a8dR7-R11)
[[2]](diffhunk://#diff-f9bbb0340f504713c99d610f3c64bf281fc13ed3cb8a1c06a5366272c9828a8dL21-R39)
* Improved error handling for missing files, empty variables, and failed
commands throughout the script, including handling cases where
`merge-base`, `git show`, or `jq` fail, and ensuring empty or missing
data does not cause the script to error out.
[[1]](diffhunk://#diff-f9bbb0340f504713c99d610f3c64bf281fc13ed3cb8a1c06a5366272c9828a8dL32-R66)
[[2]](diffhunk://#diff-f9bbb0340f504713c99d610f3c64bf281fc13ed3cb8a1c06a5366272c9828a8dR87-R108)
[[3]](diffhunk://#diff-f9bbb0340f504713c99d610f3c64bf281fc13ed3cb8a1c06a5366272c9828a8dL75-R155)

**Workflow improvements:**

* The `test-fma-darwin-pr-only.yml` and `test-fma-windows-pr-only.yml`
workflows are updated to default to "no changes" if the detection step
fails or does not set the expected output, preventing false positives or
workflow failures.
[[1]](diffhunk://#diff-28b30c8601cb7662d59efbfbbcf800cae91455fd3d875627659dced8c1257a24L70-R72)
[[2]](diffhunk://#diff-51641fd1d2cc19348b81fd8310b62ad270ca5082ceddff2d49064e78f126a1eaL76-R78)
This commit is contained in:
Allen Houchins 2026-01-05 15:01:47 -06:00 committed by GitHub
parent db4255fe47
commit b688fe3636
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 79 additions and 26 deletions

View file

@ -4,8 +4,11 @@
# This script compares the PR branch with the base branch to find:
# 1. New apps added to apps.json
# 2. Apps with changed manifest files
#
# This script always exits successfully (0) when no changes are detected.
# It only exits with error (1) for critical failures like missing jq.
set -euo pipefail
set -uo pipefail
# Get repository root
REPO_ROOT="${GITHUB_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
@ -18,9 +21,22 @@ BASE_BRANCH="${GITHUB_BASE_REF:-main}"
# Use origin/ prefix for remote branch reference
BASE_BRANCH_REF="origin/${BASE_BRANCH}"
# Check if jq is available
# Ensure GITHUB_OUTPUT exists
GITHUB_OUTPUT="${GITHUB_OUTPUT:-${REPO_ROOT}/.github_output}"
# Function to safely set outputs and exit
safe_exit() {
local has_changes="${1:-false}"
local changed_apps="${2:-[]}"
echo "CHANGED_APPS=${changed_apps}" >> "$GITHUB_OUTPUT"
echo "HAS_CHANGES=${has_changes}" >> "$GITHUB_OUTPUT"
exit 0
}
# Check if jq is available - this is a critical failure
if ! command -v jq &> /dev/null; then
echo "Error: jq is required but not installed" >&2
safe_exit "false" "[]"
exit 1
fi
@ -29,9 +45,9 @@ extract_slugs() {
local apps_file="$1"
if [ ! -f "$apps_file" ]; then
echo ""
return
return 0
fi
jq -r '.apps[].slug' "$apps_file" | sort
jq -r '.apps[].slug' "$apps_file" 2>/dev/null | sort || echo ""
}
# Function to extract app slugs from changed manifest files
@ -39,7 +55,15 @@ extract_slugs_from_changed_manifests() {
local changed_files="$1"
local slugs=()
if [ -z "$changed_files" ]; then
echo ""
return 0
fi
while IFS= read -r file; do
# Skip empty lines
[ -z "$file" ] && continue
# Extract slug from path like: outputs/app-name/darwin.json or outputs/app-name/windows.json
if [[ "$file" =~ outputs/([^/]+)/(darwin|windows)\.json$ ]]; then
app_name="${BASH_REMATCH[1]}"
@ -60,9 +84,28 @@ extract_slugs_from_changed_manifests() {
# Get changed files in outputs directory
echo "Detecting changed files in outputs directory..."
echo "Comparing HEAD with ${BASE_BRANCH_REF}..."
# Use merge-base to find the common ancestor for comparison
MERGE_BASE=$(git merge-base "${BASE_BRANCH_REF}" HEAD 2>/dev/null || echo "${BASE_BRANCH_REF}")
CHANGED_FILES=$(git diff --name-only "$MERGE_BASE" HEAD -- "ee/maintained-apps/outputs/" 2>/dev/null || echo "")
# If merge-base fails, try using the base branch ref directly
MERGE_BASE=""
if git merge-base "${BASE_BRANCH_REF}" HEAD &>/dev/null; then
MERGE_BASE=$(git merge-base "${BASE_BRANCH_REF}" HEAD 2>/dev/null || echo "")
fi
# If merge-base still failed, try the base branch ref directly
if [ -z "$MERGE_BASE" ]; then
echo "Warning: Could not find merge-base, using ${BASE_BRANCH_REF} directly"
MERGE_BASE="${BASE_BRANCH_REF}"
fi
# Get changed files, handling errors gracefully
CHANGED_FILES=""
if git diff --name-only "$MERGE_BASE" HEAD -- "ee/maintained-apps/outputs/" &>/dev/null; then
CHANGED_FILES=$(git diff --name-only "$MERGE_BASE" HEAD -- "ee/maintained-apps/outputs/" 2>/dev/null || echo "")
else
echo "Warning: Could not get changed files, assuming no changes"
CHANGED_FILES=""
fi
# Extract slugs from changed manifest files
CHANGED_MANIFEST_SLUGS=$(extract_slugs_from_changed_manifests "$CHANGED_FILES")
@ -72,37 +115,43 @@ CURRENT_SLUGS=$(extract_slugs "$APPS_JSON")
# Get base branch apps.json slugs
echo "Fetching base branch apps.json from ${MERGE_BASE}..."
BASE_APPS_JSON=$(git show "${MERGE_BASE}:ee/maintained-apps/outputs/apps.json" 2>/dev/null || echo "")
BASE_APPS_JSON=""
BASE_SLUGS=""
if [ -n "$BASE_APPS_JSON" ]; then
BASE_SLUGS=$(echo "$BASE_APPS_JSON" | jq -r '.apps[].slug' | sort)
else
echo "Warning: Could not find apps.json in base branch, treating all current apps as new"
if git show "${MERGE_BASE}:ee/maintained-apps/outputs/apps.json" &>/dev/null; then
BASE_APPS_JSON=$(git show "${MERGE_BASE}:ee/maintained-apps/outputs/apps.json" 2>/dev/null || echo "")
if [ -n "$BASE_APPS_JSON" ]; then
BASE_SLUGS=$(echo "$BASE_APPS_JSON" | jq -r '.apps[].slug' 2>/dev/null | sort || echo "")
fi
fi
# Find new slugs in apps.json
NEW_SLUGS=$(comm -13 <(echo "$BASE_SLUGS" || echo "") <(echo "$CURRENT_SLUGS" || echo "") || echo "")
if [ -z "$BASE_SLUGS" ]; then
echo "Warning: Could not find apps.json in base branch, only checking manifest file changes"
# If we can't get base slugs, only use manifest changes (don't assume all apps are new)
NEW_SLUGS=""
else
# Find new slugs in apps.json
NEW_SLUGS=$(comm -13 <(echo "$BASE_SLUGS" || echo "") <(echo "$CURRENT_SLUGS" || echo "") 2>/dev/null || echo "")
fi
# Combine all changed slugs (from manifest changes and new apps)
ALL_CHANGED_SLUGS=$(printf '%s\n' "$CHANGED_MANIFEST_SLUGS" "$NEW_SLUGS" | grep -v '^$' | sort -u)
ALL_CHANGED_SLUGS=$(printf '%s\n' "$CHANGED_MANIFEST_SLUGS" "$NEW_SLUGS" | grep -v '^$' | sort -u || echo "")
# Output results
# Output results - always exit successfully
if [ -z "$ALL_CHANGED_SLUGS" ]; then
echo "No changed apps detected."
echo "CHANGED_APPS=" >> "$GITHUB_OUTPUT"
echo "HAS_CHANGES=false" >> "$GITHUB_OUTPUT"
exit 0
safe_exit "false" "[]"
fi
echo "Detected changed apps:"
echo "$ALL_CHANGED_SLUGS" | while read -r slug; do
echo " - $slug"
[ -n "$slug" ] && echo " - $slug"
done
# Output as JSON array for GitHub Actions
CHANGED_APPS_JSON=$(echo "$ALL_CHANGED_SLUGS" | jq -R -s -c 'split("\n") | map(select(length > 0))')
CHANGED_APPS_JSON=$(echo "$ALL_CHANGED_SLUGS" | jq -R -s -c 'split("\n") | map(select(length > 0))' 2>/dev/null || echo "[]")
echo "CHANGED_APPS=$CHANGED_APPS_JSON" >> "$GITHUB_OUTPUT"
echo "HAS_CHANGES=true" >> "$GITHUB_OUTPUT"
exit 0

View file

@ -67,7 +67,9 @@ jobs:
- name: Check if there are changes
id: check-changes
run: |
if [ "${{ steps.detect-changed.outputs.HAS_CHANGES }}" == "true" ]; then
# 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

View file

@ -73,7 +73,9 @@ jobs:
- name: Check if there are changes
id: check-changes
run: |
if ("${{ steps.detect-changed.outputs.HAS_CHANGES }}" -eq "true") {
# 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 {

View file

@ -1,14 +1,14 @@
{
"versions": [
{
"version": "0.92",
"version": "0.93",
"queries": {
"exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.knollsoft.Rectangle';"
},
"installer_url": "https://github.com/rxhanson/Rectangle/releases/download/v0.92/Rectangle0.92.dmg",
"installer_url": "https://github.com/rxhanson/Rectangle/releases/download/v0.93/Rectangle0.93.dmg",
"install_script_ref": "9acc05e6",
"uninstall_script_ref": "fd2d2084",
"sha256": "d18bf60eba0dbe4d94d7b539bf3ae17c472bf71015a55d22bc55480ef888d75b",
"sha256": "848817526f3f7bd41f73cce295582523ff7bb4746ed64723575659574f298a76",
"default_categories": [
"Productivity"
]
@ -18,4 +18,4 @@
"9acc05e6": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath $INSTALLER_PATH)\")\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nhdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\"\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\"\n# copy to the applications folder\nquit_application 'com.knollsoft.Rectangle'\nif [ -d \"$APPDIR/Rectangle.app\" ]; then\n\tsudo mv \"$APPDIR/Rectangle.app\" \"$TMPDIR/Rectangle.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Rectangle.app\" \"$APPDIR\"\n",
"fd2d2084": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nquit_application 'com.knollsoft.Rectangle'\nsudo rm -rf \"$APPDIR/Rectangle.app\"\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/com.knollsoft.RectangleLauncher'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Rectangle'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.knollsoft.Rectangle'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.knollsoft.RectangleLauncher'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.knollsoft.Rectangle'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.knollsoft.Rectangle.plist'\ntrash $LOGGED_IN_USER '~/Library/WebKit/com.knollsoft.Rectangle'\n"
}
}
}