Add workflow to auto-update maintained apps list (#35387)
Introduces a GitHub Actions workflow and supporting shell script to automatically update the `fleet_maintained_apps` list in `testing-and-qa.yml` with new apps from `apps.json`. The workflow runs on a schedule or manually, installs dependencies, updates the YAML file, and creates or updates pull requests as needed.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to update the fleet_maintained_apps list in testing-and-qa.yml
|
||||
# with any new apps from apps.json.
|
||||
#
|
||||
# This script:
|
||||
# 1. Reads all available apps from apps.json
|
||||
# 2. Reads currently listed apps from testing-and-qa.yml
|
||||
# 3. Identifies missing apps
|
||||
# 4. Adds missing apps to the appropriate section (macOS or Windows)
|
||||
# 5. Maintains the existing format and comments
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Get repository root
|
||||
REPO_ROOT="${GITHUB_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||
APPS_JSON="${REPO_ROOT}/ee/maintained-apps/outputs/apps.json"
|
||||
YAML_FILE="${REPO_ROOT}/it-and-security/teams/testing-and-qa.yml"
|
||||
TEMP_FILE=$(mktemp)
|
||||
|
||||
# Check if required files exist
|
||||
if [ ! -f "$APPS_JSON" ]; then
|
||||
echo "Error: apps.json not found at $APPS_JSON" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$YAML_FILE" ]; then
|
||||
echo "Error: testing-and-qa.yml not found at $YAML_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if jq is available
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is required but not installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Loading apps from apps.json..."
|
||||
TOTAL_APPS=$(jq '.apps | length' "$APPS_JSON")
|
||||
echo "Found $TOTAL_APPS total apps in apps.json"
|
||||
|
||||
# Extract currently listed app slugs from YAML
|
||||
echo "Loading current apps from testing-and-qa.yml..."
|
||||
CURRENT_SLUGS=$(grep -E "^\s+- slug:" "$YAML_FILE" | sed 's/.*slug: \([^# ]*\).*/\1/' | tr -d ' ' | sort -u)
|
||||
CURRENT_COUNT=$(echo "$CURRENT_SLUGS" | grep -c . || true)
|
||||
echo "Found $CURRENT_COUNT currently listed apps"
|
||||
|
||||
# Find missing apps and separate by platform
|
||||
DARWIN_APPS=()
|
||||
WINDOWS_APPS=()
|
||||
|
||||
while IFS= read -r app_json; do
|
||||
slug=$(echo "$app_json" | jq -r '.slug')
|
||||
if ! echo "$CURRENT_SLUGS" | grep -q "^${slug}$"; then
|
||||
platform=$(echo "$app_json" | jq -r '.platform')
|
||||
name=$(echo "$app_json" | jq -r '.name')
|
||||
if [ "$platform" = "darwin" ]; then
|
||||
DARWIN_APPS+=("${slug}|${name}")
|
||||
elif [ "$platform" = "windows" ]; then
|
||||
WINDOWS_APPS+=("${slug}|${name}")
|
||||
fi
|
||||
fi
|
||||
done < <(jq -c '.apps[]' "$APPS_JSON")
|
||||
|
||||
# Sort apps by slug for consistency
|
||||
if [ ${#DARWIN_APPS[@]} -gt 0 ]; then
|
||||
IFS=$'\n'
|
||||
DARWIN_APPS=($(printf '%s\n' "${DARWIN_APPS[@]}" | sort))
|
||||
unset IFS
|
||||
fi
|
||||
|
||||
if [ ${#WINDOWS_APPS[@]} -gt 0 ]; then
|
||||
IFS=$'\n'
|
||||
WINDOWS_APPS=($(printf '%s\n' "${WINDOWS_APPS[@]}" | sort))
|
||||
unset IFS
|
||||
fi
|
||||
|
||||
TOTAL_MISSING=$((${#DARWIN_APPS[@]} + ${#WINDOWS_APPS[@]}))
|
||||
|
||||
if [ $TOTAL_MISSING -eq 0 ]; then
|
||||
echo "No new apps to add. Everything is up to date!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found $TOTAL_MISSING new apps to add:"
|
||||
for app in "${DARWIN_APPS[@]+"${DARWIN_APPS[@]}"}" "${WINDOWS_APPS[@]+"${WINDOWS_APPS[@]}"}"; do
|
||||
slug="${app%%|*}"
|
||||
name="${app#*|}"
|
||||
echo " - $name ($slug)"
|
||||
done
|
||||
|
||||
# Find the fleet_maintained_apps section boundaries
|
||||
FLEET_APPS_START=$(grep -n "fleet_maintained_apps:" "$YAML_FILE" | head -1 | cut -d: -f1)
|
||||
if [ -z "$FLEET_APPS_START" ]; then
|
||||
echo "Error: Could not find fleet_maintained_apps section in YAML file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find where the fleet_maintained_apps section ends (next top-level key or end of file)
|
||||
TOTAL_LINES=$(wc -l < "$YAML_FILE")
|
||||
FLEET_APPS_END=$TOTAL_LINES
|
||||
for ((i=$((FLEET_APPS_START + 1)); i<=TOTAL_LINES; i++)); do
|
||||
line=$(sed -n "${i}p" "$YAML_FILE")
|
||||
# Check if this is a top-level key (starts at column 0, not indented)
|
||||
if [[ "$line" =~ ^[a-zA-Z] ]] && [[ ! "$line" =~ ^[[:space:]] ]]; then
|
||||
FLEET_APPS_END=$((i - 1))
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Find macOS and Windows comment lines
|
||||
MACOS_COMMENT_LINE=$(awk -v start="$FLEET_APPS_START" -v end="$FLEET_APPS_END" 'NR >= start && NR <= end && /# macOS apps/ {print NR; exit}' "$YAML_FILE")
|
||||
WINDOWS_COMMENT_LINE=$(awk -v start="$FLEET_APPS_START" -v end="$FLEET_APPS_END" 'NR >= start && NR <= end && /# Windows apps/ {print NR; exit}' "$YAML_FILE")
|
||||
|
||||
# Find last darwin app and first windows app
|
||||
LAST_DARWIN_LINE=$(awk -v start="$FLEET_APPS_START" -v end="$FLEET_APPS_END" 'NR >= start && NR <= end && /\/darwin/ {last=NR} END {print last+0}' "$YAML_FILE")
|
||||
FIRST_WINDOWS_LINE=$(awk -v start="$FLEET_APPS_START" -v end="$FLEET_APPS_END" 'NR >= start && NR <= end && /\/windows/ {print NR; exit}' "$YAML_FILE")
|
||||
|
||||
# Build the updated file
|
||||
{
|
||||
# Copy everything before fleet_maintained_apps
|
||||
head -n $((FLEET_APPS_START - 1)) "$YAML_FILE"
|
||||
|
||||
# Copy the fleet_maintained_apps: line
|
||||
sed -n "${FLEET_APPS_START}p" "$YAML_FILE"
|
||||
|
||||
# Handle macOS section
|
||||
# Copy existing macOS apps (from start to last darwin or windows comment)
|
||||
MACOS_SECTION_END=$FLEET_APPS_END
|
||||
if [ -n "$WINDOWS_COMMENT_LINE" ]; then
|
||||
MACOS_SECTION_END=$((WINDOWS_COMMENT_LINE - 1))
|
||||
elif [ -n "$FIRST_WINDOWS_LINE" ]; then
|
||||
MACOS_SECTION_END=$((FIRST_WINDOWS_LINE - 1))
|
||||
fi
|
||||
|
||||
if [ "$MACOS_SECTION_END" -gt "$FLEET_APPS_START" ]; then
|
||||
# Copy existing macOS section
|
||||
sed -n "$((FLEET_APPS_START + 1)),${MACOS_SECTION_END}p" "$YAML_FILE"
|
||||
fi
|
||||
|
||||
# Add macOS comment if we have new macOS apps and comment doesn't exist
|
||||
if [ ${#DARWIN_APPS[@]} -gt 0 ] && [ -z "$MACOS_COMMENT_LINE" ] && [ -z "$LAST_DARWIN_LINE" ]; then
|
||||
echo " # macOS apps"
|
||||
fi
|
||||
|
||||
# Add new macOS apps
|
||||
if [ ${#DARWIN_APPS[@]} -gt 0 ]; then
|
||||
for app in "${DARWIN_APPS[@]}"; do
|
||||
slug="${app%%|*}"
|
||||
name="${app#*|}"
|
||||
printf " - slug: %s # %s for macOS\n" "$slug" "$name"
|
||||
echo " self_service: true"
|
||||
done
|
||||
fi
|
||||
|
||||
# Handle Windows section
|
||||
# Add Windows comment if needed
|
||||
if [ -n "$WINDOWS_COMMENT_LINE" ]; then
|
||||
# Copy Windows comment line
|
||||
sed -n "${WINDOWS_COMMENT_LINE}p" "$YAML_FILE"
|
||||
# Copy existing Windows apps
|
||||
if [ "$WINDOWS_COMMENT_LINE" -lt "$FLEET_APPS_END" ]; then
|
||||
sed -n "$((WINDOWS_COMMENT_LINE + 1)),${FLEET_APPS_END}p" "$YAML_FILE"
|
||||
fi
|
||||
elif [ -n "$FIRST_WINDOWS_LINE" ]; then
|
||||
# No Windows comment, but there are Windows apps
|
||||
# Copy existing Windows apps
|
||||
sed -n "${FIRST_WINDOWS_LINE},${FLEET_APPS_END}p" "$YAML_FILE"
|
||||
elif [ ${#WINDOWS_APPS[@]} -gt 0 ]; then
|
||||
# No existing Windows apps, add comment
|
||||
echo " # Windows apps"
|
||||
fi
|
||||
|
||||
# Add new Windows apps
|
||||
if [ ${#WINDOWS_APPS[@]} -gt 0 ]; then
|
||||
for app in "${WINDOWS_APPS[@]}"; do
|
||||
slug="${app%%|*}"
|
||||
name="${app#*|}"
|
||||
printf " - slug: %s # %s for Windows\n" "$slug" "$name"
|
||||
echo " self_service: true"
|
||||
done
|
||||
fi
|
||||
|
||||
# Copy everything after fleet_maintained_apps section
|
||||
if [ "$FLEET_APPS_END" -lt "$TOTAL_LINES" ]; then
|
||||
tail -n +$((FLEET_APPS_END + 1)) "$YAML_FILE"
|
||||
fi
|
||||
|
||||
} > "$TEMP_FILE"
|
||||
|
||||
# Replace original file
|
||||
mv "$TEMP_FILE" "$YAML_FILE"
|
||||
|
||||
echo "Successfully added $TOTAL_MISSING new apps to testing-and-qa.yml"
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
name: Update Testing & QA Maintained Apps
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run twice daily at 6:00 AM and 6:00 PM UTC
|
||||
- cron: '0 6 * * *'
|
||||
- cron: '0 18 * * *'
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
# This allows a subsequently queued workflow run to interrupt previous runs
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
|
||||
shell: bash
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update-apps:
|
||||
permissions:
|
||||
contents: write # Required to push new branch
|
||||
pull-requests: write # Required to open PRs
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install jq
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq
|
||||
|
||||
- name: Update Testing & QA Apps
|
||||
id: update-apps
|
||||
run: |
|
||||
.github/scripts/dogfood-update-testing-qa-apps.sh
|
||||
if [ $? -eq 0 ]; then
|
||||
# Check if there are any changes
|
||||
if git diff --quiet it-and-security/teams/testing-and-qa.yml; then
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
echo "No changes detected"
|
||||
else
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
echo "Changes detected"
|
||||
git diff it-and-security/teams/testing-and-qa.yml
|
||||
fi
|
||||
else
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
- name: Search for Existing PRs
|
||||
if: steps.update-apps.outputs.changed == 'true'
|
||||
id: search_pr
|
||||
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
|
||||
with:
|
||||
script: |
|
||||
const { data: pullRequests } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100
|
||||
});
|
||||
const matchingPRs = pullRequests.filter(pr =>
|
||||
pr.title.includes('Update Testing & QA maintained apps') ||
|
||||
pr.title.includes('Update Testing and QA maintained apps')
|
||||
);
|
||||
return matchingPRs.map(pr => pr.number);
|
||||
|
||||
- name: Configure Git
|
||||
if: steps.update-apps.outputs.changed == 'true'
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.update-apps.outputs.changed == 'true'
|
||||
id: create-pr
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: |
|
||||
Update Testing & QA maintained apps
|
||||
|
||||
Automatically added new Fleet-maintained apps to the Testing & QA team configuration.
|
||||
title: "Update Testing & QA maintained apps"
|
||||
body: |
|
||||
This PR automatically updates the `fleet_maintained_apps` list in `testing-and-qa.yml` with any new apps from Fleet's maintained apps library.
|
||||
|
||||
The changes were generated automatically by the [dogfood-update-testing-qa-apps workflow](https://github.com/${{ github.repository }}/actions/workflows/dogfood-update-testing-qa-apps.yml).
|
||||
branch: update-testing-qa-apps-$(date +%s)
|
||||
delete-branch: true
|
||||
assignees: allenhouchins
|
||||
|
||||
- name: Close Existing PRs
|
||||
if: steps.update-apps.outputs.changed == 'true' && steps.search_pr.outputs.result != '[]'
|
||||
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
|
||||
with:
|
||||
script: |
|
||||
const prNumbers = JSON.parse('${{ steps.search_pr.outputs.result }}');
|
||||
const newPrNumber = '${{ steps.create-pr.outputs.pull-request-number }}';
|
||||
for (const prNumber of prNumbers) {
|
||||
if (prNumber.toString() !== newPrNumber) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: `Closing in favor of #${newPrNumber}.`,
|
||||
});
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
state: 'closed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user