diff --git a/.github/scripts/dogfood-update-testing-qa-apps.sh b/.github/scripts/dogfood-update-testing-qa-apps.sh new file mode 100644 index 0000000000..3adf284c0e --- /dev/null +++ b/.github/scripts/dogfood-update-testing-qa-apps.sh @@ -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" + diff --git a/.github/workflows/dogfood-update-testing-qa-apps.yml b/.github/workflows/dogfood-update-testing-qa-apps.yml new file mode 100644 index 0000000000..c68440e044 --- /dev/null +++ b/.github/workflows/dogfood-update-testing-qa-apps.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', + }); + } + } + + +