feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2025 The Fuchsia Authors
|
||||
#
|
||||
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
|
||||
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
|
||||
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
script_name="ci/check_actions.sh"
|
||||
|
||||
# Ensure action-validator is installed
|
||||
if [ ! -x "$HOME/.cargo/bin/action-validator" ]; then
|
||||
echo "$script_name: action-validator not found, installing..." >&2
|
||||
# Install specific version to ensure reproducibility
|
||||
cargo install -q action-validator --version 0.8.0 --locked
|
||||
fi
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
|
||||
# Files to exclude from validation (e.g., because they are not Actions/Workflows)
|
||||
# Use relative paths matching `find .github` output
|
||||
EXCLUDE_FILES=(
|
||||
"./.github/dependabot.yml"
|
||||
"./.github/release.yml"
|
||||
)
|
||||
|
||||
failed=0
|
||||
|
||||
# Use process substitution and while loop to handle filenames with spaces robustly
|
||||
while IFS= read -r -d '' file; do
|
||||
# Check if file is in exclusion list
|
||||
for exclude in "${EXCLUDE_FILES[@]}"; do
|
||||
if [[ "$file" == "$exclude" ]]; then
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
|
||||
if ! output=$(action-validator "$file" 2>&1); then
|
||||
echo "$script_name: ❌ Validation failed for $file" >&2
|
||||
echo "$output" | sed "s|^|$script_name: |" >&2
|
||||
failed=1
|
||||
fi
|
||||
done < <(find ./.github -type f \( -iname '*.yml' -o -iname '*.yaml' \) -print0)
|
||||
|
||||
if [[ $failed -ne 0 ]]; then
|
||||
echo "$script_name: One or more files failed validation." >&2
|
||||
exit 1
|
||||
fi
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2024 The Fuchsia Authors
|
||||
#
|
||||
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
|
||||
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
|
||||
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
# Check whether the set of toolchains tested in this file (other than
|
||||
# 'msrv', 'stable', and 'nightly') is equal to the set of toolchains
|
||||
# listed in the 'package.metadata.build-rs' section of Cargo.toml.
|
||||
#
|
||||
# If the inputs to `diff` are not identical, `diff` exits with a
|
||||
# non-zero error code, which causes this script to fail (thanks to
|
||||
# `set -e`).
|
||||
diff \
|
||||
<(yq -r '.jobs.build_test.strategy.matrix.toolchain | .[]' .github/workflows/ci.yml | \
|
||||
sort -u | grep -v '^\(msrv\|stable\|nightly\)$') \
|
||||
<(cargo metadata -q --format-version 1 | \
|
||||
jq -r ".packages[] | select(.name == \"zerocopy\").metadata.\"build-rs\" | keys | .[]" | \
|
||||
sort -u) >&2
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2024 The Fuchsia Authors
|
||||
#
|
||||
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
|
||||
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
|
||||
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
if [[ "$1" == "--fix" ]]; then
|
||||
FMT_FLAGS=""
|
||||
else
|
||||
FMT_FLAGS="--check"
|
||||
fi
|
||||
|
||||
find . -iname '*.rs' -type f -not -path './target/*' -not -iname '*.expected.rs' -not -path './vendor/*' -not -path './tools/vendor/*' -print0 | xargs -0 --no-run-if-empty ./cargo.sh +nightly fmt $FMT_FLAGS -- >&2
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2024 The Fuchsia Authors
|
||||
#
|
||||
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
|
||||
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
|
||||
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
set -eo pipefail
|
||||
which yq > /dev/null
|
||||
jobs=$(for i in $(find .github -iname '*.yaml' -or -iname '*.yml')
|
||||
do
|
||||
# Select jobs that are triggered by pull request.
|
||||
if yq -e '.on | has("pull_request")' "$i" 2>/dev/null >/dev/null
|
||||
then
|
||||
# This gets the list of jobs that all-jobs-succeed does not depend on.
|
||||
comm -23 \
|
||||
<(yq -r '.jobs | keys | .[]' "$i" | sort | uniq) \
|
||||
<(yq -r '.jobs["all-jobs-succeed"].needs[]' "$i" | sort | uniq)
|
||||
fi
|
||||
|
||||
# The grep call here excludes all-jobs-succeed from the list of jobs that
|
||||
# all-jobs-succeed does not depend on. If all-jobs-succeed does
|
||||
# not depend on itself, we do not care about it.
|
||||
done | sort | uniq | (grep -v '^all-jobs-succeed$' || true)
|
||||
)
|
||||
|
||||
if [ -n "$jobs" ]
|
||||
then
|
||||
missing_jobs="$(echo "$jobs" | tr ' ' '\n')"
|
||||
echo "all-jobs-succeed missing dependencies on some jobs: $missing_jobs" | tee -a $GITHUB_STEP_SUMMARY >&2
|
||||
exit 1
|
||||
fi
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2025 The Fuchsia Authors
|
||||
#
|
||||
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
|
||||
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
|
||||
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
read -r -d '' PYTHON_SCRIPT <<'EOF' || true
|
||||
import sys
|
||||
import json
|
||||
|
||||
def parse_version(v):
|
||||
"""Converts a version string to a tuple of integers."""
|
||||
return tuple(map(int, v.split(".")))
|
||||
|
||||
def main():
|
||||
"""
|
||||
Checks that the package's MSRV is strictly lower than any version
|
||||
specified in `package.metadata.build-rs`.
|
||||
"""
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing JSON from cargo metadata: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Find the zerocopy package
|
||||
try:
|
||||
pkg = next(p for p in data["packages"] if p["name"] == "zerocopy")
|
||||
except StopIteration:
|
||||
print("Error: zerocopy package not found in metadata", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
msrv_str = pkg.get("rust_version")
|
||||
if not msrv_str:
|
||||
print("Error: rust-version not found in Cargo.toml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
msrv = parse_version(msrv_str)
|
||||
except ValueError:
|
||||
print(f"Error: Invalid MSRV format: {msrv_str}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
build_rs_versions = (pkg.get("metadata") or {}).get("build-rs", {})
|
||||
|
||||
failed = False
|
||||
for name, ver_str in build_rs_versions.items():
|
||||
try:
|
||||
ver = parse_version(ver_str)
|
||||
except ValueError:
|
||||
print(f"Warning: Skipping invalid version format for {name}: {ver_str}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
# Check that MSRV < Version (strictly lower)
|
||||
if not (msrv < ver):
|
||||
print(f"Error: MSRV ({msrv_str}) is not strictly lower than {name} ({ver_str})", file=sys.stderr)
|
||||
failed = True
|
||||
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
EOF
|
||||
|
||||
cargo metadata --format-version 1 --no-deps | python3 -c "$PYTHON_SCRIPT"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2024 The Fuchsia Authors
|
||||
#
|
||||
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
|
||||
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
|
||||
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
# Install again in case the installation failed during the
|
||||
# `generate_cache` step. We treat that step as best-effort and
|
||||
# suppress all errors from it.
|
||||
cargo install -q cargo-readme --version 3.2.0
|
||||
|
||||
diff <(cargo -q run --config tools/.cargo/config.toml --manifest-path tools/Cargo.toml -p generate-readme) README.md >&2
|
||||
exit $?
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2026 The Fuchsia Authors
|
||||
#
|
||||
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
|
||||
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
|
||||
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
# Directories to search
|
||||
DIRS=("tests" "zerocopy-derive/tests")
|
||||
|
||||
EXIT_CODE=0
|
||||
|
||||
for dir in "${DIRS[@]}"; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Warning: Directory $dir does not exist, skipping." >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# Find all .stderr files
|
||||
while IFS= read -r -d '' stderr_file; do
|
||||
# Strip .stderr extension
|
||||
base="${stderr_file%.stderr}"
|
||||
|
||||
# Strip toolchain suffixes
|
||||
base="${base%.msrv}"
|
||||
base="${base%.stable}"
|
||||
base="${base%.nightly}"
|
||||
|
||||
# Construct the corresponding .rs file path
|
||||
rs_file="${stderr_file%.stderr}.rs"
|
||||
rs_file="${base}.rs"
|
||||
|
||||
# Check if the .rs file exists. The `-e` flag checks if file exists:
|
||||
# It returns true for regular files and valid symlinks, and false for
|
||||
# broken symlinks or missing files.
|
||||
if [ ! -e "$rs_file" ]; then
|
||||
echo "Error: Orphaned stderr file found: $stderr_file" >&2
|
||||
echo " Missing regular file or valid symlink: $rs_file" >&2
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
done < <(find "$dir" -name "*.stderr" -print0)
|
||||
done
|
||||
|
||||
if [ "$EXIT_CODE" -eq 1 ]; then
|
||||
echo "Found stale .stderr files. Please delete them." >&2
|
||||
fi
|
||||
|
||||
exit "$EXIT_CODE"
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2025 The Fuchsia Authors
|
||||
#
|
||||
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
|
||||
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
|
||||
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# This allows us to leave XODO comments in this file and have them still be
|
||||
# picked up by this script without having the script itself trigger false
|
||||
# positives. The alternative would be to exclude this script entirely, which
|
||||
# would mean that we couldn't use XODO comments in this script.
|
||||
KEYWORD=$(echo XODO | sed -e 's/X/T/')
|
||||
|
||||
# Make sure `rg` is installed (if this fails, `set -e` above will cause the
|
||||
# script to exit).
|
||||
rg --version >/dev/null
|
||||
|
||||
# -H: Print filename (default for multiple files/recursive)
|
||||
# -n: Print line number
|
||||
# -w: Match whole words
|
||||
# Match TODO, TODO-check-disable, and TODO-check-enable
|
||||
PATTERN="$KEYWORD|$KEYWORD-check-disable|$KEYWORD-check-enable"
|
||||
output=$(rg -H -n -w "$PATTERN" "$@" || true)
|
||||
|
||||
commit_output=$(git log -1 --pretty=%B 2>/dev/null | rg -n -w "$PATTERN" || true)
|
||||
if [ -n "$commit_output" ]; then
|
||||
commit_output=$(echo "$commit_output" | sed "s/^/COMMIT_MESSAGE:/")
|
||||
output="$commit_output${output:+$'\n'$output}"
|
||||
fi
|
||||
|
||||
if [ -z "$output" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Track the disabled state for each file. Since we process lines in order for
|
||||
# each file, we can maintain state. However, rg output might interleave files if
|
||||
# not sorted, but usually it's grouped. To be safe, we sort the output by
|
||||
# filename and line number.
|
||||
sorted_output=$(echo "$output" | sort -t: -k1,1 -k2,2n)
|
||||
|
||||
exit_code=0
|
||||
current_file=""
|
||||
disabled=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^(.*):([0-9]+):(.*)$ ]]; then
|
||||
file="${BASH_REMATCH[1]}"
|
||||
content="${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "Error: could not parse rg output line: $line" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$file" != "$current_file" ]; then
|
||||
current_file="$file"
|
||||
disabled=0
|
||||
fi
|
||||
|
||||
if [[ "$content" == *"$KEYWORD-check-disable"* ]]; then
|
||||
disabled=1
|
||||
elif [[ "$content" == *"$KEYWORD-check-enable"* ]]; then
|
||||
disabled=0
|
||||
elif [[ "$content" == *"$KEYWORD"* ]]; then
|
||||
if [ "$disabled" -eq 0 ]; then
|
||||
if [ "$exit_code" -eq 0 ]; then
|
||||
echo "Found $KEYWORD markers in the codebase." >&2
|
||||
echo "$KEYWORD is used for tasks that should be done before merging a PR; if you want to leave a message in the codebase, use FIXME." >&2
|
||||
echo "If you need to allow a $KEYWORD, wrap it in $KEYWORD-check-disable and $KEYWORD-check-enable." >&2
|
||||
echo "" >&2
|
||||
exit_code=1
|
||||
fi
|
||||
echo "$line" >&2
|
||||
fi
|
||||
fi
|
||||
done <<< "$sorted_output"
|
||||
|
||||
exit $exit_code
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2024 The Fuchsia Authors
|
||||
#
|
||||
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
|
||||
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
|
||||
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
# Usage: version <crate-name>
|
||||
function version {
|
||||
cargo metadata -q --format-version 1 | jq -r ".packages[] | select(.name == \"$1\").version"
|
||||
}
|
||||
|
||||
ver_zerocopy=$(version zerocopy)
|
||||
ver_zerocopy_derive=$(version zerocopy-derive)
|
||||
|
||||
# Usage: dependency-version <kind> <target>
|
||||
function dependency-version {
|
||||
KIND="$1"
|
||||
TARGET="$2"
|
||||
cargo metadata -q --format-version 1 \
|
||||
| jq -r ".packages[] | select(.name == \"zerocopy\").dependencies[] | select((.name == \"zerocopy-derive\") and .kind == $KIND and .target == $TARGET).req"
|
||||
}
|
||||
|
||||
# The non-dev dependency version (kind `null` filters out the dev
|
||||
# dependency, and target `null` filters out the targeted version).
|
||||
zerocopy_derive_dep_ver=$(dependency-version null null)
|
||||
|
||||
# The non-dev dependency, targeted version (kind `null` filters out
|
||||
# the dev dependency).
|
||||
zerocopy_derive_targeted_ver=$(dependency-version null '"cfg(any())"')
|
||||
|
||||
# The dev dependency version (kind `"dev"` selects only the dev
|
||||
# dependency).
|
||||
zerocopy_derive_dev_dep_ver=$(dependency-version '"dev"' null)
|
||||
|
||||
function assert-match {
|
||||
VER_A="$1"
|
||||
VER_B="$2"
|
||||
SUCCESS_MSG="$3"
|
||||
FAILURE_MSG="$4"
|
||||
if [[ "$VER_A" == "$VER_B" ]]; then
|
||||
echo "$SUCCESS_MSG" | tee -a $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "$FAILURE_MSG" | tee -a $GITHUB_STEP_SUMMARY >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert-match "$ver_zerocopy" "$ver_zerocopy_derive" \
|
||||
"Same crate version ($ver_zerocopy) found for zerocopy and zerocopy-derive." \
|
||||
"Different crate versions found for zerocopy ($ver_zerocopy) and zerocopy-derive ($ver_zerocopy_derive)."
|
||||
|
||||
# Note the leading `=` sign - the dependency needs to be an exact one.
|
||||
assert-match "=$ver_zerocopy_derive" "$zerocopy_derive_dep_ver" \
|
||||
"zerocopy depends upon same version of zerocopy-derive in-tree ($zerocopy_derive_dep_ver)." \
|
||||
"zerocopy depends upon different version of zerocopy-derive ($zerocopy_derive_dep_ver) than the one in-tree ($ver_zerocopy_derive)."
|
||||
|
||||
# Note the leading `=` sign - the dependency needs to be an exact one.
|
||||
assert-match "=$ver_zerocopy_derive" "$zerocopy_derive_dev_dep_ver" \
|
||||
"In dev mode, zerocopy depends upon same version of zerocopy-derive in-tree ($zerocopy_derive_dev_dep_ver)." \
|
||||
"In dev mode, zerocopy depends upon different version of zerocopy-derive ($zerocopy_derive_dev_dep_ver) than the one in-tree ($ver_zerocopy_derive)."
|
||||
|
||||
assert-match "$zerocopy_derive_dep_ver" "$zerocopy_derive_targeted_ver" \
|
||||
"Same crate version ($zerocopy_derive_dep_ver) found for optional and targeted zerocopy-derive dependency." \
|
||||
"Different crate versions found for optional ($zerocopy_derive_dep_ver) and targeted ($zerocopy_derive_targeted_ver) dependency."
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2024 The Fuchsia Authors
|
||||
#
|
||||
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
|
||||
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
|
||||
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
set -e
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 <version>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
|
||||
sed -i -e "s/^zerocopy-derive = { version = \"=[0-9a-zA-Z\.-]*\"/zerocopy-derive = { version = \"=$VERSION\"/" Cargo.toml
|
||||
sed -i -e "s/^version = \"[0-9a-zA-Z\.-]*\"/version = \"$VERSION\"/" Cargo.toml zerocopy-derive/Cargo.toml
|
||||
|
||||
# Ensure that `Cargo.lock` is updated.
|
||||
cargo generate-lockfile
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import posixpath
|
||||
import os
|
||||
|
||||
# Exit codes
|
||||
SUCCESS = 0
|
||||
NOT_APPROVED = 1
|
||||
TECHNICAL_ERROR = 255
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate PR changes against auto-approver rules."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default=".github/auto-approvers.json",
|
||||
help="Path to the rules JSON.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--changed-files", help="Path to the fetched changed files JSON."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expected-count", type=int, help="Total number of files expected in the PR."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--contributors", nargs="+", help="List of GitHub usernames to validate."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-config",
|
||||
action="store_true",
|
||||
help="Only validate the configuration file and exit.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# REGEX: Strict path structure, prevents absolute paths and weird characters
|
||||
VALID_PATH = re.compile(r"^([a-zA-Z0-9_.-]+/)+$")
|
||||
|
||||
# Load and validate config
|
||||
try:
|
||||
with open(args.config) as f:
|
||||
rules = json.load(f)
|
||||
except FileNotFoundError:
|
||||
print(f"::error::❌ Config file not found at {args.config}")
|
||||
sys.exit(TECHNICAL_ERROR)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"::error::❌ Failed to parse config JSON: {e}")
|
||||
sys.exit(TECHNICAL_ERROR)
|
||||
|
||||
safe_rules = {}
|
||||
for directory, users in rules.items():
|
||||
if not isinstance(users, list):
|
||||
print(
|
||||
f"::error::❌ Users for '{directory}' must be a JSON array (list), not a string."
|
||||
)
|
||||
sys.exit(TECHNICAL_ERROR)
|
||||
|
||||
if not VALID_PATH.match(directory) or ".." in directory.split("/"):
|
||||
print(f"::error::❌ Invalid config path: {directory}")
|
||||
sys.exit(TECHNICAL_ERROR)
|
||||
|
||||
safe_rules[directory] = [str(u).lower() for u in users]
|
||||
|
||||
if not args.check_config:
|
||||
# Validate that required arguments are present if not in --check-config mode
|
||||
if not (
|
||||
args.changed_files and args.expected_count is not None and args.contributors
|
||||
):
|
||||
print(
|
||||
"::error::❌ Missing required arguments: --changed-files, --expected-count, and --contributors are required unless --check-config is used."
|
||||
)
|
||||
sys.exit(TECHNICAL_ERROR)
|
||||
|
||||
# Load and flatten changed files
|
||||
try:
|
||||
with open(args.changed_files) as f:
|
||||
file_objects = json.load(f)
|
||||
except FileNotFoundError:
|
||||
print(f"::error::❌ Changed files JSON not found at {args.changed_files}")
|
||||
sys.exit(TECHNICAL_ERROR)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"::error::❌ Failed to parse changed files JSON: {e}")
|
||||
sys.exit(TECHNICAL_ERROR)
|
||||
|
||||
if not file_objects or len(file_objects) != args.expected_count:
|
||||
print(
|
||||
f"::error::❌ File truncation mismatch or empty PR. Expected {args.expected_count}, got {len(file_objects) if file_objects else 0}."
|
||||
)
|
||||
sys.exit(TECHNICAL_ERROR)
|
||||
|
||||
if not all(isinstance(obj, list) for obj in file_objects):
|
||||
print("::error::❌ Invalid payload format. Expected a list of lists.")
|
||||
sys.exit(TECHNICAL_ERROR)
|
||||
|
||||
changed_files = [path for obj in file_objects for path in obj]
|
||||
|
||||
# Validate every file against every contributor
|
||||
contributors = set(str(c).lower() for c in args.contributors)
|
||||
print(f"👥 Validating contributors: {', '.join(contributors)}")
|
||||
|
||||
for raw_file_path in changed_files:
|
||||
file_path = posixpath.normpath(raw_file_path)
|
||||
|
||||
# Find the most specific (longest) matching directory rule.
|
||||
longest_match_dir = None
|
||||
for directory in safe_rules.keys():
|
||||
if file_path.startswith(directory):
|
||||
if longest_match_dir is None or len(directory) > len(
|
||||
longest_match_dir
|
||||
):
|
||||
longest_match_dir = directory
|
||||
|
||||
# First, explicitly fail if the file isn't covered by ANY rule.
|
||||
if not longest_match_dir:
|
||||
print(
|
||||
f"::error::❌ File '{file_path}' does not fall under any configured auto-approve directory."
|
||||
)
|
||||
sys.exit(NOT_APPROVED)
|
||||
|
||||
# Then, verify every contributor has access to that specific rule.
|
||||
for user in contributors:
|
||||
if user not in safe_rules[longest_match_dir]:
|
||||
print(
|
||||
f"::error::❌ Contributor @{user} not authorized for '{file_path}'."
|
||||
)
|
||||
sys.exit(NOT_APPROVED)
|
||||
|
||||
if args.check_config:
|
||||
print("✅ Configuration is structurally valid")
|
||||
else:
|
||||
print("✅ Validation passed")
|
||||
|
||||
sys.exit(SUCCESS)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user