feat: linux_analyze in a workflow (#187889)

fixes #187817

- Will run the same as a LUCI recipe, but in GitHub actions
- Updated composite-flutter-setup to install/cache ktlint.
- Should detect and wait for engine builds before running

Successful run:
https://github.com/flutter/flutter/actions/runs/27382050611/job/80920878955?pr=187889#annotation:15:3


## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.
diff --git a/.github/actions/composite-flutter-setup/action.yml b/.github/actions/composite-flutter-setup/action.yml
index d60e8d1..61c1a81 100644
--- a/.github/actions/composite-flutter-setup/action.yml
+++ b/.github/actions/composite-flutter-setup/action.yml
@@ -70,7 +70,7 @@
         run: |
           echo "revision=${{inputs.ANDROID_TOOLS_VERSION}};${{inputs.ANDROID_PLATFORM}};build-tools;${{inputs.ANDROID_BUILDTOOLS}}" >> "$GITHUB_OUTPUT"
       - name: Android SDK Cache
-        uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684
+        uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae
         id: android-sdk-setup
         if: env.ACT
         with:
@@ -90,6 +90,37 @@
           echo "/opt/android/sdk/cmdline-tools/${{ inputs.ANDROID_TOOLS_VERSION }}/bin" >> "$GITHUB_PATH"
           echo "/opt/android/sdk/platform-tools" >> "$GITHUB_PATH"
 
+      # ktlint is needed by tree analyze
+      - name: ktlint cache
+        id: ktlint-cache
+        uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae
+        with:
+          # Cache the ktlint binary in runner.temp to avoid interfering with Flutter's bin folder
+          path: ${{ runner.temp }}/ktlint-bin/ktlint
+          # Hardcode the version in the key since it's hardcoded in the URL
+          key: ${{ runner.os }}-ktlint-1.5.0
+
+      - name: Download and Install ktlint 1.5.0
+        if: steps.ktlint-cache.outputs.cache-hit != 'true'
+        shell: bash
+        run: |
+          mkdir -p ${{ runner.temp }}/ktlint-bin
+
+          # 1. Download directly into the temp directory
+          curl -fsSL -o ${{ runner.temp }}/ktlint-bin/ktlint https://github.com/ktlint/ktlint/releases/download/1.5.0/ktlint
+
+          # 2. Verify the checksum to ensure the binary hasn't been tampered with
+          EXPECTED_SHA="a16be01dcc480aab2f55f444b620142152f66e31564b3b9376506d624c28a2ad"
+          echo "$EXPECTED_SHA  ${{ runner.temp }}/ktlint-bin/ktlint" | sha256sum --check -
+
+          # 3. Make the binary executable
+          chmod a+x ${{ runner.temp }}/ktlint-bin/ktlint
+
+      - name: Add ktlint to PATH
+        shell: bash
+        run: |
+          echo "${{ runner.temp }}/ktlint-bin" >> "$GITHUB_PATH"
+
       - name: Add `flutter` to the PATH
         shell: bash
         run: |
@@ -108,7 +139,7 @@
         run: |
           echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
       - name: Flutter artifacts cache
-        uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684
+        uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae
         id: flutter-artifacts
         with:
           path: ${{ github.workspace }}/bin/cache
@@ -122,7 +153,7 @@
           find dev examples packages -name "pubspec.yaml" -print0 | sort -z | xargs -0 cat | sha256sum  >> "$RUNNER_TEMP/pub_deps_sha"
           echo "revision=$(cat "$RUNNER_TEMP/pub_deps_sha")" >> "$GITHUB_OUTPUT"
       - name: pub package cache
-        uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684
+        uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae
         id: pub-cache
         with:
           path: |
diff --git a/.github/scripts/did_engine_change.sh b/.github/scripts/did_engine_change.sh
new file mode 100755
index 0000000..c80256b
--- /dev/null
+++ b/.github/scripts/did_engine_change.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+# Copyright 2014 The Flutter Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# did_engine_change.sh
+#
+# Reads a list of files from stdin and prints "true" if any file matches
+# the engine pattern, or "false" otherwise.
+#
+# Usage:
+#   .github/scripts/git_files_changed.sh <hash|ref> | .github/scripts/did_engine_change.sh
+#
+# Test:
+#
+# Pass ("true")
+# ```shell
+# cat <<EOF | .github/scripts/did_engine_change.sh
+# packages/flutter/lib/src/material/button.dart
+# engine/src/flutter/shell/common/shell.cc
+# dev/bots/test.dart
+# EOF
+# echo "DEPS" | .github/scripts/did_engine_change.sh
+# echo "bin/internal/content_aware_hash.sh" | .github/scripts/did_engine_change.sh
+# ```
+#
+# Fail ("false")
+# ```shell
+# cat <<EOF | .github/scripts/did_engine_change.sh
+# packages/flutter/lib/src/material/button.dart
+# dev/bots/test.dart
+# EOF
+# ```
+
+if grep -qE "^(DEPS|engine/.*|bin/internal/content_aware_hash\.(ps1|sh))$"; then
+  echo "true"
+else
+  echo "false"
+fi
diff --git a/.github/scripts/git_files_changed.sh b/.github/scripts/git_files_changed.sh
new file mode 100755
index 0000000..a5ea87b
--- /dev/null
+++ b/.github/scripts/git_files_changed.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# Copyright 2014 The Flutter Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# git_files_changed.sh <hash|ref>
+# Returns a list of files that change from the given hash or ref compared to HEAD.
+#
+# Example:
+#  ./.github/scripts/git_files_changed.sh upstream/master
+#  ./.github/scripts/git_files_changed.sh eccbe7d4f7ac6ebf35d342db58e37736ed6c60f9
+if [ -z "$1" ]; then
+  echo "Usage: $0 <hash|ref>" >&2
+  exit 1
+fi
+
+BASE_REF="$1"
+
+# Get the changed files using triple-dot syntax
+# This automatically finds the common ancestor and ignores unrelated changes on the base branch
+git diff --name-only "$BASE_REF"...HEAD
diff --git a/.github/workflows/tree-analyze.yml b/.github/workflows/tree-analyze.yml
new file mode 100644
index 0000000..b2b3801
--- /dev/null
+++ b/.github/workflows/tree-analyze.yml
@@ -0,0 +1,76 @@
+name: Tree Analyze
+
+on:
+  pull_request:
+    branches: [master]
+  merge_group:
+    branches: [master]
+
+jobs:
+  Tree_analyze:
+    permissions:
+      contents: read
+    runs-on: ubuntu-latest
+    timeout-minutes: 130
+
+    steps:
+      # Note: we must check out the tree for the composite action to be available
+      - name: Checkout code
+        if: env.ACT != 'true'
+        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+        with:
+          fetch-depth: 0
+          ref: ${{ github.event.pull_request.head.sha || github.sha }}
+
+      - name: Checkout code (act local)
+        if: env.ACT
+        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+
+      - name: Check Engine Changes
+        id: check_engine
+        run: |
+          # 1. Determine local base tracking SHA
+          if [ "${{ github.event_name }}" = "pull_request" ]; then
+            BASE_REF="${{ github.event.pull_request.base.sha }}"
+          elif [ "${{ github.event_name }}" = "merge_group" ]; then
+            BASE_REF="${{ github.event.merge_group.base_sha }}"
+          else
+            BASE_REF="origin/master"
+          fi
+
+          echo "Comparing against base SHA: $BASE_REF"
+
+          # 2. Get the changed files using the helper script
+          CHANGED_FILES=$(.github/scripts/git_files_changed.sh "$BASE_REF")
+          echo "Changed files in this branch:"
+          echo "$CHANGED_FILES"
+
+          # 3. Check if any file matches engine pattern using the helper script:
+          if [ "$(echo "$CHANGED_FILES" | .github/scripts/did_engine_change.sh)" = "true" ]; then
+            echo "Engine file changed - forced to wait for Linux linux_host_engine"
+            echo "changed=true" >> "$GITHUB_OUTPUT"
+          else
+            echo "changed=false" >> "$GITHUB_OUTPUT"
+          fi
+
+      - name: Wait for test check-in
+        if: steps.check_engine.outputs.changed == 'true' && env.ACT != 'true'
+        uses: lewagon/wait-on-check-action@9312864dfbc9fd208e9c0417843430751c042800
+        with:
+          ref: ${{ github.event.pull_request.head.sha || github.sha }}
+          check-name: 'Linux linux_host_engine'
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
+          wait-interval: 30 # seconds
+          checks-discovery-timeout: 3600 # Wait up to 1 hour for LUCI to register the check
+          allowed-conclusions: success,neutral
+          verbose: false
+
+      - name: Redirect engine.version
+        if: steps.check_engine.outputs.changed == 'true' && github.event_name == 'pull_request'
+        run: echo "FLUTTER_REALM=flutter_archives_v2" >> $GITHUB_ENV
+
+      - uses: ./.github/actions/composite-flutter-setup
+
+      - name: Run Analyze
+        run: |
+          SHARD=analyze LUCI_CI=true dart --enable-asserts dev/bots/test.dart
diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart
index 81b77e4..dad6843 100644
--- a/dev/bots/analyze.dart
+++ b/dev/bots/analyze.dart
@@ -2771,6 +2771,8 @@
 const Set<String> kExecutableAllowlist = <String>{
   '.autoroller-preupload.sh',
   '.claude/skills',
+  '.github/scripts/did_engine_change.sh',
+  '.github/scripts/git_files_changed.sh',
   'bin/dart',
   'bin/flutter',
   'bin/flutter-dev',