Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/scripts/affected-list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python
# Infer all affected features based on a list of changed features
#
# Usage in GitHub Actions:
#
# .github/scripts/affected-list.py 'changed_features_json' 'all_features_json'
#
# Usage in terminal:
#
# .github/scripts/affected-list.py '["macos-brew"]' "$(GITHUB_OUTPUT=/dev/null .github/scripts/all-features.sh)"

import json
import os
import sys
from collections import defaultdict


# A helper function to recursively collect all dependents of a feature
def collect_dependents(
dependency_map: dict[str, set[str]], feature: str, visited: set[str]
) -> set[str]:
for dependent in dependency_map[feature]:
if dependent not in visited:
visited.add(dependent)
collect_dependents(dependency_map, dependent, visited)
return visited


# Build a dependency map from all features
# Format: {feature_a: [feature_b_depends_on_a, feature_c_depends_on_a, ...]}
def parse_dependencies(all_features: dict[str, list[str]]) -> dict[str, set[str]]:
dependency_map: dict[str, set[str]] = defaultdict(set)

# Convert dependencies to a reverse map
for feature, dependencies in all_features.items():
for dep in dependencies:
dependency_map[dep].add(feature)

# Flatten trees of dependencies
# E.g., if A -> B and B -> C, then A -> B,C
flattened: dict[str, set[str]] = defaultdict(set)
for feature in all_features:
flattened[feature] = collect_dependents(dependency_map, feature, set())

return flattened


def infer_dependencies(
changed_features: list[str], dependency_map: dict[str, set[str]]
) -> list[str]:
affected_features: set[str] = set(changed_features)
for feature in changed_features:
affected_features.update(dependency_map[feature])
return sorted(affected_features)


def output_to_github_actions(affected_features: list[str]) -> None:
github_output_path = os.getenv("GITHUB_OUTPUT")
if github_output_path:
with open(github_output_path, "a") as gh_out:
gh_out.write(f"features={json.dumps(affected_features)}\n")


def main() -> None:
# Parse input arguments
changed_features = list(json.loads(sys.argv[1]))
all_features = json.loads(sys.argv[2])

# Build the dependency map
dependency_map = parse_dependencies(all_features)

# Infer affected features
affected_features = infer_dependencies(changed_features, dependency_map)

print("Affected features:")
print(json.dumps(affected_features))

# Output to GitHub Actions
output_to_github_actions(affected_features)


if __name__ == "__main__":
main()
60 changes: 60 additions & 0 deletions .github/scripts/all-features.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/bin/bash
# .github/scripts/all-features.sh
# Outputs all features and their dependencies from Makefile

set -euo pipefail

# Extract features and their dependencies from Makefile
features_with_deps=$(
{ make -prRn -f Makefile : 2>/dev/null || true; } |
awk '
function trim(s) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", s)
return s
}
function is_valid(name) {
return !(name == "" || name ~ /^\.|%/ || name ~ /^\//)
}
$0 == "# Files" { in_files = 1; next }
$0 ~ /^# files hash-table stats:/ { in_files = 0 }
!in_files { next }
$0 ~ /^# Not a target:/ { skip_next = 1; next }
/^[^#[:space:]].*:/ {
line = $0
if (skip_next) { skip_next = 0; next }
split(line, parts, ":")
name = trim(parts[1])
if (!is_valid(name)) { next }
deps = trim(substr(line, index(line, ":") + 1))
gsub(/[[:space:]]+/, " ", deps)
if (deps == "") {
print name
next
}
print name " " deps
}
' |
sort -u
)

# Convert to JSON
features_json=$(
printf '%s\n' "$features_with_deps" |
jq -R -s -c '
split("\n")
| map(select(length>0))
| map(
split(" ")
| { (.[0]): (.[1:] | map(select(length>0))) }
)
| add
'
)

echo "All features and dependencies:" >&2
echo "$features_with_deps" >&2

# Output to GitHub Actions
echo "features=$features_json" >> "$GITHUB_OUTPUT"

echo $features_json
49 changes: 49 additions & 0 deletions .github/scripts/changed-files.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/bin/bash
# .github/scripts/changed-files.sh
# Outputs changed files between base and head commits for GitHub Actions

set -euo pipefail

# Read GitHub context from first argument
if [ $# -lt 1 ]; then
echo "Usage: $0 <github_context_json>" >&2
exit 1
fi

GITHUB_CONTEXT="$1"

# Parse event information using jq
event_name=$(echo "$GITHUB_CONTEXT" | jq -r '.event_name')

# Determine base and head commits based on event type
if [ "$event_name" = "pull_request" ]; then
base=$(echo "$GITHUB_CONTEXT" | jq -r '.event.pull_request.base.sha')
head=$(echo "$GITHUB_CONTEXT" | jq -r '.event.pull_request.head.sha')
elif [ "$event_name" = "push" ]; then
base=$(echo "$GITHUB_CONTEXT" | jq -r '.event.before')
head=$(echo "$GITHUB_CONTEXT" | jq -r '.sha')
else
echo "Unsupported event: $event_name" >&2
base=""
head=""
fi

# Check if we have valid base and head commits
if [ -z "$base" ] || [ -z "$head" ] || [ "$base" = "null" ] || [ "$head" = "null" ]; then
echo "No base/head to diff; leaving paths empty." >&2
echo "paths=" >> "$GITHUB_OUTPUT"
exit 0
fi

# Get changed files using git diff
files=$(git diff --name-only "$base" "$head" || true)

echo "Changed files:" >&2
echo "$files" >&2

# Output to GitHub Actions
{
echo 'paths<<EOF'
echo "$files"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
32 changes: 32 additions & 0 deletions .github/scripts/feature-list.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/bash
# .github/scripts/feature-list.sh
# Generates feature list from changed file paths

set -euo pipefail

# Read changed paths from first argument
if [ $# -lt 1 ]; then
echo "Usage: $0 <changed_paths>" >&2
exit 1
fi

changed_paths="$1"

# Extract feature names for any changed file under pkg/some-feature/*
changed_features=$(
printf '%s\n' "$changed_paths" |
sed -n 's|^pkg/\([^/]*\).*|\1|p' |
sort -u
)

echo "Changed features:" >&2
echo "$changed_features" >&2

# Convert to JSON array
features_json=$(
printf '%s\n' "$changed_features" |
jq -R -s -c 'split("\n") | map(select(length>0))'
)

# Output to GitHub Actions
echo "features=$features_json" >> "$GITHUB_OUTPUT"
76 changes: 76 additions & 0 deletions .github/scripts/merge-features.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/bin/bash
# .github/scripts/merge-features.sh
# Merge passed in features with a list of fixed features

set -euo pipefail

FIXED_FEATURES=(
# ansible
# clean
# cli-network
# core
git
# javascript
# macos-brew
# macos-c-cpp
# macos-clean
# macos-cli-network
# macos-cli-useful
# macos-core
# macos-docker
# macos-duti
# macos-file-handler
# macos-finder
# macos-git
# macos-icon
# macos-javascript
# macos-markedit
# macos-micro
# macos-nano
# macos-popclip
# macos-quicklook
# macos-screensaver
# macos-shellcheck
# macos-stow
# macos-sublime
# macos-terminal
# macos-touch-id-sudo
# macos-vscode
# macos-zsh
# micro
# nano
# python
# stow
# zsh
)

if [ $# -lt 1 ]; then
echo "Usage: $0 <features_json>" >&2
exit 1
fi

features_json="$1"

if [ -z "$features_json" ] || [ "$features_json" = "null" ]; then
features_json="[]"
fi

if ! echo "$features_json" | jq -e '.' >/dev/null 2>&1; then
echo "Invalid JSON for features." >&2
exit 1
fi

fixed_json=$(
printf '%s\n' "${FIXED_FEATURES[@]}" |
jq -R -s -c 'split("\n") | map(select(length>0))'
)

merged_json=$(
echo "$features_json" |
jq -c --argjson fixed "$fixed_json" '. + $fixed | sort | unique'
)

echo "Merged features:" >&2
echo "$merged_json" | jq -r '.[]' >&2

echo "features=$merged_json" >> "$GITHUB_OUTPUT"
Loading
Loading