Browse Source

ci: harden kernel automation (#286)

Trinadh Thatakula 6 days ago
parent
commit
0202350c76

+ 97 - 9
.github/actions/apply-device-patches/action.yml

@@ -1,5 +1,6 @@
 name: 'Apply Device-Specific Patches'
 description: 'Apply WiFi/Bluetooth fixes for Samsung and Xiaomi 6.6 GKI devices'
+
 inputs:
   version:
     description: 'Kernel config version (e.g., android15-6.6)'
@@ -16,18 +17,105 @@ runs:
       shell: bash
       working-directory: ${{ github.workspace }}/kernel/common
       run: |
-        SYMBOL_LIST=android/abi_gki_aarch64_galaxy
-        echo "kdp_set_cred_non_rcu" >> $SYMBOL_LIST
-        echo "kdp_usecount_dec_and_test" >> $SYMBOL_LIST
-        echo "kdp_usecount_inc" >> $SYMBOL_LIST
+        set -euo pipefail
 
         PATCH="${{ github.workspace }}/kernel_patches/samsung/min_kdp/add-min_kdp-symbols.patch"
-        if patch -p1 --dry-run < "$PATCH"; then
-          patch -p1 --no-backup-if-mismatch < $PATCH
-        fi
+        MIN_KDP="${{ github.workspace }}/kernel_patches/samsung/min_kdp/min_kdp.c"
+        test -f "$PATCH" || { echo "Missing Samsung min_kdp patch: $PATCH" >&2; exit 1; }
+        test -f "$MIN_KDP" || { echo "Missing Samsung min_kdp source: $MIN_KDP" >&2; exit 1; }
+
+        SYMBOL_LIST=android/abi_gki_aarch64_galaxy
+        TRANSACTION_DIR="$(mktemp -d)"
+        BACKUP_DIR="$TRANSACTION_DIR/original"
+        STAGE_DIR="$TRANSACTION_DIR/stage"
+        TRANSACTION_COMMITTED=false
+        declare -a TRANSACTION_PATHS
+        TRANSACTION_PATH_COUNT=0
+
+        add_transaction_path() {
+          local path="$1"
+          case "$path" in
+            /dev/null|..|../*|*/../*|/*)
+              echo "Unsafe Samsung patch path: $path" >&2
+              exit 1
+              ;;
+          esac
+          local index existing
+          for ((index = 0; index < TRANSACTION_PATH_COUNT; index++)); do
+            existing="${TRANSACTION_PATHS[index]}"
+            [ "$existing" = "$path" ] && return
+          done
+          TRANSACTION_PATHS[TRANSACTION_PATH_COUNT]="$path"
+          TRANSACTION_PATH_COUNT=$((TRANSACTION_PATH_COUNT + 1))
+        }
+
+        restore_transaction() {
+          set +e
+          set +u
+          if [ "$TRANSACTION_COMMITTED" = true ]; then
+            rm -rf "$TRANSACTION_DIR"
+            return
+          fi
+          local index path
+          for ((index = 0; index < TRANSACTION_PATH_COUNT; index++)); do
+            path="${TRANSACTION_PATHS[index]}"
+            if [[ -e "$BACKUP_DIR/$path" || -L "$BACKUP_DIR/$path" ]]; then
+              rm -rf "$path"
+              mkdir -p "$(dirname "$path")"
+              cp -a "$BACKUP_DIR/$path" "$path"
+            else
+              rm -rf "$path"
+            fi
+          done
+          rm -rf "$TRANSACTION_DIR"
+        }
+        trap restore_transaction EXIT
+
+        PATCH_PATHS=()
+        while IFS= read -r path; do
+          [ -n "$path" ] && PATCH_PATHS+=("$path")
+        done < <(awk '/^(---|\+\+\+) / { path=$2; sub(/^[ab]\//, "", path); print path }' "$PATCH" | sort -u)
+        for path in "${PATCH_PATHS[@]}"; do
+          add_transaction_path "$path"
+        done
+        add_transaction_path "$SYMBOL_LIST"
+        add_transaction_path drivers/min_kdp.c
+        add_transaction_path drivers/Makefile
+
+        for path in "${TRANSACTION_PATHS[@]}"; do
+          if [[ -e "$path" || -L "$path" ]]; then
+            mkdir -p "$BACKUP_DIR/$(dirname "$path")" "$STAGE_DIR/$(dirname "$path")"
+            cp -a "$path" "$BACKUP_DIR/$path"
+            cp -a "$path" "$STAGE_DIR/$path"
+          else
+            mkdir -p "$STAGE_DIR/$(dirname "$path")"
+          fi
+        done
+
+        (
+          cd "$STAGE_DIR"
+          patch -p1 --dry-run < "$PATCH"
+          patch -p1 --no-backup-if-mismatch < "$PATCH"
+          printf '%s\n' \
+            'kdp_set_cred_non_rcu' \
+            'kdp_usecount_dec_and_test' \
+            'kdp_usecount_inc' >> "$SYMBOL_LIST"
+          cp "$MIN_KDP" drivers/min_kdp.c
+          if ! grep -Fqx 'obj-y += min_kdp.o' drivers/Makefile; then
+            printf '%s\n' 'obj-y += min_kdp.o' >> drivers/Makefile
+          fi
+        )
 
-        cp "${{ github.workspace }}/kernel_patches/samsung/min_kdp/min_kdp.c" drivers/min_kdp.c
-        echo "obj-y += min_kdp.o" >> drivers/Makefile
+        for path in "${TRANSACTION_PATHS[@]}"; do
+          if [[ -e "$STAGE_DIR/$path" || -L "$STAGE_DIR/$path" ]]; then
+            mkdir -p "$(dirname "$path")"
+            rm -rf "$path"
+            cp -a "$STAGE_DIR/$path" "$path"
+          else
+            rm -rf "$path"
+          fi
+        done
+        TRANSACTION_COMMITTED=true
 
     - name: Fix WiFi and Bluetooth on Xiaomi 6.6 GKI Devices
       if: inputs.version == 'android15-6.6'

+ 3 - 3
.github/actions/build-kernel/action.yml

@@ -27,11 +27,11 @@ runs:
         OUT_DIR="/home/runner/out" \
         LTO=thin \
         BUILD_CONFIG=common/build.config.gki.aarch64 \
-        build/build.sh -j"$(nproc)" \
         CC="/usr/bin/ccache clang" \
-        CXX="/usr/bin/ccache clang+" \
+        CXX="/usr/bin/ccache clang++" \
         HOSTCC="/usr/bin/ccache clang" \
-        HOSTCXX="/usr/bin/ccache clang+"
+        HOSTCXX="/usr/bin/ccache clang++" \
+        build/build.sh -j"$(nproc)"
       else
         sed -i '/name = "kernel_aarch64",/a\    check_defconfig = "disabled",' common/BUILD.bazel
         tools/bazel build \

+ 19 - 4
.github/actions/extract-sublevel-file-name/action.yml

@@ -27,15 +27,30 @@ runs:
   steps:
     - id: extract
       shell: bash
+      env:
+        INPUT_OS_PATCH_LEVEL: ${{ inputs.os_patch_level }}
       run: |
         set -euo pipefail
 
         SUBLEVEL="${{ inputs.sublevel }}"
-        if [ -f "${{ github.workspace }}/kernel/common/Makefile" ]; then
-          EXTRACTED="$(grep '^SUBLEVEL = ' "${{ github.workspace }}/kernel/common/Makefile" | awk '{print $3}')"
-          if [ "${{ inputs.os_patch_level }}" = "lts" ] && [ -n "$EXTRACTED" ]; then
-            SUBLEVEL="$EXTRACTED"
+        MAKEFILE="${{ github.workspace }}/kernel/common/Makefile"
+        if [ "$(printf '%s' "$INPUT_OS_PATCH_LEVEL" | tr '[:upper:]' '[:lower:]')" = "lts" ]; then
+          if [ ! -f "$MAKEFILE" ]; then
+            echo "LTS build requires $MAKEFILE" >&2
+            exit 1
           fi
+          mapfile -t matches < <(grep -E '^[[:space:]]*SUBLEVEL[[:space:]]*=' "$MAKEFILE" || true)
+          if [ "${#matches[@]}" -ne 1 ]; then
+            echo "Expected exactly one SUBLEVEL assignment in $MAKEFILE; found ${#matches[@]}" >&2
+            exit 1
+          fi
+          EXTRACTED="${matches[0]#*=}"
+          EXTRACTED="$(printf '%s' "$EXTRACTED" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
+          if ! [[ "$EXTRACTED" =~ ^[0-9]+$ ]]; then
+            echo "Invalid numeric SUBLEVEL '$EXTRACTED' in $MAKEFILE" >&2
+            exit 1
+          fi
+          SUBLEVEL="$EXTRACTED"
         fi
 
         FILE_NAME="${{ inputs.kernel_version }}.${SUBLEVEL}-${{ inputs.android_version }}-${{ inputs.os_patch_level }}"

+ 13 - 5
.github/scripts/update-supported-devices.py

@@ -5,6 +5,12 @@ import re, sys, datetime, pathlib
 ISSUE_BODY = pathlib.Path(sys.argv[1]).read_text() if len(sys.argv) > 1 else sys.stdin.read()
 MD = pathlib.Path("docs/supported-devices.md")
 
+def validate_cell(name, value):
+    if any(ch in value for ch in ('|', '\r', '\n')):
+        print(f"invalid {name}: Markdown delimiters and line breaks are not allowed", file=sys.stderr)
+        raise SystemExit(1)
+    return value
+
 def field(id):
     # issue form bodies render as "### <Label>\n\nvalue"
     # we match by id label text variations
@@ -39,12 +45,14 @@ heading_map = {
 }
 
 manufacturer = field("manufacturer") or "Other"
-device = field("device").strip()
-codename = field("codename").strip()
-gki = field("gki_kernel").strip()
-firmware = field("firmware").strip() or "stock"
-status = field("status").strip() or "Supported"
 custom_oem = field("custom_manufacturer").strip()
+if manufacturer == "Other":
+    custom_oem = validate_cell("custom_oem", custom_oem)
+device = validate_cell("device", field("device").strip())
+codename = validate_cell("codename", field("codename").strip())
+gki = validate_cell("gki", field("gki_kernel").strip())
+firmware = validate_cell("firmware", field("firmware").strip() or "stock")
+status = validate_cell("status", field("status").strip() or "Supported")
 # handle custom OEM when Other is selected
 if manufacturer == "Other" and custom_oem and custom_oem.lower() not in ("none", "_no response_", ""):
     manufacturer = custom_oem.strip().title()

+ 36 - 11
.github/scripts/update_verified_pins.py

@@ -12,6 +12,7 @@ import os
 import re
 import sys
 from datetime import datetime, timezone
+from uuid import uuid4
 
 REPO = os.environ.get("GITHUB_WORKSPACE", ".")
 MAIN = os.path.join(REPO, ".github/workflows/main.yml")
@@ -35,12 +36,21 @@ def approved(env):
 
 def read_pins():
     """Return dict {var_name: sha} from the current PIN_* block in main.yml."""
-    with open(MAIN, "r", encoding="utf-8") as fh:
+    with open(MAIN, "r", encoding="utf-8", newline="") as fh:
         text = fh.read()
     pins = {}
     for var, _, _ in PINS:
-        m = re.search(r"%s=\"([0-9a-f]{40})\"" % re.escape(var), text)
-        pins[var] = m.group(1) if m else None
+        pattern = re.compile(
+            rf'^[ \t]*{re.escape(var)}(?P<assignment>[ \t]*=[^\r\n]*)?[ \t]*(?=\r?$)',
+            re.MULTILINE,
+        )
+        matches = list(pattern.finditer(text))
+        if len(matches) != 1:
+            raise ValueError(f"expected exactly one {var} assignment, found {len(matches)}")
+        value = re.fullmatch(r'="([0-9a-f]{40})"[ \t]*', matches[0].group("assignment") or "")
+        if value is None:
+            raise ValueError(f"invalid {var} assignment")
+        pins[var] = value.group(1)
     return pins
 
 
@@ -68,7 +78,7 @@ def history_path():
     ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%SZ")
     d = os.path.join(REPO, ".github/pins/history")
     os.makedirs(d, exist_ok=True)
-    return os.path.join(d, f"{ts}.json")
+    return os.path.join(d, f"{ts}-{uuid4().hex[:8]}.json")
 
 
 def write_history(changes, path):
@@ -83,15 +93,30 @@ def write_history(changes, path):
 
 
 def apply_pins(promoted):
-    with open(MAIN, "r", encoding="utf-8") as fh:
+    with open(MAIN, "r", encoding="utf-8", newline="") as fh:
         text = fh.read()
+    replacements = []
     for key, sha in promoted.items():
-        pattern = re.compile(r"%s=\"([0-9a-f]{40})\"" % re.escape(key))
-        text, n = pattern.subn(lambda m: f"{key}=\"{sha}\"", text, count=1)
-        if n == 0:
-            print(f"  ERROR: could not locate pin for {key}", file=sys.stderr)
-            return False
-    with open(MAIN, "w", encoding="utf-8") as fh:
+        pattern = re.compile(
+            rf'^[ \t]*{re.escape(key)}(?P<assignment>[ \t]*=[^\r\n]*)?[ \t]*(?=\r?$)',
+            re.MULTILINE,
+        )
+        matches = list(pattern.finditer(text))
+        if len(matches) != 1:
+            raise ValueError(f"expected exactly one {key} assignment, found {len(matches)}")
+        match = matches[0]
+        value = re.fullmatch(r'="([0-9a-f]{40})"[ \t]*', match.group("assignment") or "")
+        if value is None:
+            raise ValueError(f"invalid {key} assignment")
+        line = match.group(0)
+        indentation = line[: len(line) - len(line.lstrip(" \t"))]
+        trailing = line[len(line.rstrip(" \t")) :]
+        replacements.append((match.start(), match.end(), f'{indentation}{key}="{sha}"{trailing}'))
+
+    for start, end, replacement in reversed(replacements):
+        text = text[:start] + replacement + text[end:]
+
+    with open(MAIN, "w", encoding="utf-8", newline="") as fh:
         fh.write(text)
     return True
 

+ 4 - 0
.github/workflows/main.yml

@@ -816,6 +816,10 @@ jobs:
     secrets: inherit
 
   update-verified-pins:
+    concurrency:
+      group: update-verified-pins-${{ github.repository }}
+      queue: max
+      cancel-in-progress: false
     if: "github.event_name == 'workflow_dispatch' && !inputs.test_release_notes && inputs.commit_mode == 'update'"
     runs-on: ubuntu-latest
     permissions:

+ 4 - 2
.github/workflows/prepare.yml

@@ -89,6 +89,8 @@ jobs:
       - name: Read Config and Build Final Matrix
         id: final
         shell: bash
+        env:
+          INPUT_OS_PATCH_LEVEL: ${{ inputs.os_patch_level }}
         run: |
           if [ ! -f "${{ inputs.config_file }}" ]; then
             echo "Error: Config file not found: ${{ inputs.config_file }}"
@@ -133,8 +135,8 @@ jobs:
               )
             }')
 
-          SELECTED_PATCH_LEVEL="${{ inputs.os_patch_level }}"
-          if [ "$(printf '%s' "$SELECTED_PATCH_LEVEL" | tr '[:upper:]' '[:lower:]')" = "all" ]; then
+          SELECTED_PATCH_LEVEL="$(printf '%s' "$INPUT_OS_PATCH_LEVEL" | tr '[:upper:]' '[:lower:]')"
+          if [ "$SELECTED_PATCH_LEVEL" = "all" ]; then
             SELECTED_PATCH_LEVEL=""
           fi
           if [ -n "$SELECTED_PATCH_LEVEL" ]; then