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
211 changes: 97 additions & 114 deletions .github/scripts/check-unprotected-changes.sh
Original file line number Diff line number Diff line change
@@ -1,87 +1,64 @@
#!/bin/bash
# This file was generated using AI assistance (Cursor AI) and reviewed by the maintainers.
#
# Detect files under code/ changed since the last rebase that have no rebase
# rule. Such modifications would be silently lost on the next upstream rebase.
# Detect files under code/ changed in a PR whose modifications are NOT fully
# protected by rebase rules. Two categories:
# - "Missing rule" — no rebase rule exists for the file at all
# - "Incomplete rule" — a rule exists but doesn't reproduce the current content
#
# How it works:
# 1. Finds the last rebase commit (message starts with "Rebase against the upstream")
# 2. Lists files in code/ changed since that commit
# 3. Filters out Che-only additions (che-* extensions, che/ subdirs) and lockfiles
# 4. For remaining files, checks whether a rebase rule exists in:
# - resolve_conflicts() elif chain in rebase.sh
# - .rebase/replace/ (from→by rules)
# - .rebase/add/ (JSON fragments merged in)
# - .rebase/override/ (JSON overrides)
# 5. Reports any uncovered files
# For "incomplete" detection the script runs the actual rebase handler against
# upstream content and diffs the result against the working tree, reusing the
# test infrastructure from .claude/skills/test-rebase-rules/.
#
# Usage:
# bash check-unprotected-changes.sh # default: changes since last rebase
# bash check-unprotected-changes.sh <base>..<head> # explicit commit range
# bash check-unprotected-changes.sh --verbose # show covered files too
# bash check-unprotected-changes.sh --pr-comment # output as GitHub PR comment (markdown)
# bash check-unprotected-changes.sh --list # output bare file paths only
# bash check-unprotected-changes.sh --pr-comment <base>..<head>
#
# Prerequisites (for incomplete-rule detection):
# - upstream-code remote must be fetched for the current upstream version
#
# Exit codes:
# 0 — All modifications are covered by rebase rules (or are Che-only additions)
# 1 — Unprotected modifications found
# 2 — Error (can't find rebase commit, etc.)
# 0 — All modifications are fully covered
# 1 — Problems found
# 2 — Error
set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$REPO_ROOT"

VERBOSE=false
OUTPUT_MODE="text"
COMMIT_RANGE=""
for arg in "$@"; do
case "$arg" in
--verbose|-v) VERBOSE=true ;;
--pr-comment) OUTPUT_MODE="pr-comment" ;;
--list) OUTPUT_MODE="list" ;;
*..*) COMMIT_RANGE="$arg" ;;
esac
done

# --- Locate the last rebase commit ---
if [ -z "$COMMIT_RANGE" ]; then
REBASE_COMMIT=$(git log --format='%H' --grep="^Rebase against the upstream" -1)
if [ -z "$REBASE_COMMIT" ]; then
if [ "$OUTPUT_MODE" = "list" ]; then
exit 2
fi
echo "ERROR: Could not find a rebase commit in the history."
echo "Expected a commit whose message starts with 'Rebase against the upstream'."
echo "ERROR: Could not find a rebase commit and no commit range provided." >&2
exit 2
fi
REBASE_MSG=$(git log -1 --format='%s' "$REBASE_COMMIT")
COMMIT_RANGE="${REBASE_COMMIT}..HEAD"
fi

if [ "$OUTPUT_MODE" = "text" ]; then
if [ -n "${REBASE_MSG:-}" ]; then
echo "=== Unprotected Changes Check ==="
echo "Base: $REBASE_COMMIT (${REBASE_MSG})"
else
echo "=== Unprotected Changes Check ==="
echo "Range: $COMMIT_RANGE"
fi
echo ""
fi
UPSTREAM_VERSION=$(grep '^CURRENT_UPSTREAM_VERSION=' rebase.sh | head -1 | sed 's/.*="\(.*\)"/\1/')

HANDLER_SCRIPT="$REPO_ROOT/.claude/skills/test-rebase-rules/test-rebase-handler.sh"
RUN_ALL_TESTS="$REPO_ROOT/.claude/skills/test-rebase-rules/run-all-tests.sh"

# --- Build the set of files covered by rebase rules ---
COVERED_LIST=$(mktemp)
trap 'rm -f "$COVERED_LIST"' EXIT

# 1. Parse resolve_conflicts() elif chain for explicit file paths.
while IFS= read -r line; do
if [[ "$line" =~ \[\[.*==.*\"([^\"]+)\".*\]\] ]]; then
echo "${BASH_REMATCH[1]}"
fi
done < <(sed -n '/^resolve_conflicts()/,/^}/p' rebase.sh) >> "$COVERED_LIST"

# 2. .rebase/replace/ rules
find .rebase/replace -name '*.json' -type f 2>/dev/null | while IFS= read -r rule_file; do
code_path="${rule_file#.rebase/replace/}"
code_path="${code_path%.json}"
Expand All @@ -91,103 +68,109 @@ find .rebase/replace -name '*.json' -type f 2>/dev/null | while IFS= read -r rul
echo "$code_path"
done >> "$COVERED_LIST"

# 3. .rebase/add/ rules
find .rebase/add -type f 2>/dev/null | while IFS= read -r rule_file; do
echo "${rule_file#.rebase/add/}"
done >> "$COVERED_LIST"

# 4. .rebase/override/ rules
find .rebase/override -type f 2>/dev/null | while IFS= read -r rule_file; do
echo "${rule_file#.rebase/override/}"
done >> "$COVERED_LIST"
find .rebase/add -type f 2>/dev/null | while IFS= read -r f; do echo "${f#.rebase/add/}"; done >> "$COVERED_LIST"
find .rebase/override -type f 2>/dev/null | while IFS= read -r f; do echo "${f#.rebase/override/}"; done >> "$COVERED_LIST"

sort -u -o "$COVERED_LIST" "$COVERED_LIST"

# --- Check changed files ---
UNCOVERED=()
COVERED_COUNT=0
SKIPPED_COUNT=0
# --- Collect changed files and classify ---
MISSING_RULE=()
TO_TEST=()

while IFS= read -r file_path; do
# Skip package-lock.json (handled by resolve_package_lock during rebase)
case "$file_path" in
*/package-lock.json) ((SKIPPED_COUNT++)); continue ;;
esac

# Skip Che-only additions — these don't exist in upstream and won't conflict
case "$file_path" in
code/extensions/che-*) ((SKIPPED_COUNT++)); continue ;;
*/che/*.ts|*/che/*.js) ((SKIPPED_COUNT++)); continue ;;
*/package-lock.json) continue ;;
code/extensions/che-*) continue ;;
*/che/*.ts|*/che/*.js) continue ;;
esac

if grep -qxF "$file_path" "$COVERED_LIST"; then
((COVERED_COUNT++))
continue
TO_TEST+=("$file_path")
else
MISSING_RULE+=("$file_path")
fi

UNCOVERED+=("$file_path")
done < <(git diff --name-only "$COMMIT_RANGE" -- code/)

# --- Output ---
# --- Test files that have rules: are rules complete? ---
INCOMPLETE_RULE=()
HAS_UPSTREAM=false
if [ -n "$UPSTREAM_VERSION" ]; then
git rev-parse "upstream-code/$UPSTREAM_VERSION" > /dev/null 2>&1 && HAS_UPSTREAM=true
fi

if [ "$HAS_UPSTREAM" = true ] && [ ${#TO_TEST[@]} -gt 0 ] && [ -f "$RUN_ALL_TESTS" ]; then
TEST_OUTPUT=$(bash "$RUN_ALL_TESTS" "${TO_TEST[@]}" 2>&1) || true

# --list mode: bare file paths, nothing else
if [ "$OUTPUT_MODE" = "list" ]; then
for f in "${UNCOVERED[@]+${UNCOVERED[@]}}"; do
echo "$f"
done
[ ${#UNCOVERED[@]} -eq 0 ] && exit 0 || exit 1
while IFS= read -r line; do
if [[ "$line" =~ ^\|\ \`([^\`]+)\`\ \| ]]; then
failed_file="${BASH_REMATCH[1]}"
INCOMPLETE_RULE+=("$failed_file")
fi
done < <(echo "$TEST_OUTPUT" | sed -n '/^### Failures/,/^### /p' | grep '^| `')
fi

# --pr-comment mode: markdown for a GitHub PR comment
# --- Output ---
TOTAL_ISSUES=$(( ${#MISSING_RULE[@]} + ${#INCOMPLETE_RULE[@]} ))

if [ "$OUTPUT_MODE" = "pr-comment" ]; then
if [ ${#UNCOVERED[@]} -eq 0 ]; then
if [ $TOTAL_ISSUES -eq 0 ]; then
exit 0
fi
cat <<'HEADER'
<!-- rebase-rules-check -->
### Missing Rebase Rules

The following files were modified in this PR but do not have corresponding rebase rules.
Without these rules, the changes **will be lost** during the next upstream rebase.

| # | File |
|---|------|
HEADER
i=1
for f in "${UNCOVERED[@]}"; do
echo "| $i | \`$f\` |"
((i++))
done
echo ""
echo "Comment \`/add-rebase-rules\` on this PR to add the missing rules automatically."
exit 1
fi

# Default text mode
if [ "$VERBOSE" = true ]; then
echo "Covered: $COVERED_COUNT | Skipped: $SKIPPED_COUNT | Uncovered: ${#UNCOVERED[@]}"
echo "<!-- rebase-rules-check -->"
echo "### Rebase Rules Check"
echo ""
echo "Rebase rules on file:"
sed 's/^/ /' "$COVERED_LIST"
echo "The following files were modified in this PR but their changes are **not fully protected** by rebase rules."
echo "Without proper rules, these changes **will be lost** during the next upstream rebase."
echo ""

if [ ${#MISSING_RULE[@]} -gt 0 ]; then
echo "#### Missing rules (no rebase rule exists)"
echo ""
echo "| # | File |"
echo "|---|------|"
i=1
for f in "${MISSING_RULE[@]}"; do
echo "| $i | \`$f\` |"
((i++))
done
echo ""
fi

if [ ${#INCOMPLETE_RULE[@]} -gt 0 ]; then
echo "#### Incomplete rules (rule exists but doesn't cover new changes)"
echo ""
echo "| # | File |"
echo "|---|------|"
i=1
for f in "${INCOMPLETE_RULE[@]}"; do
echo "| $i | \`$f\` |"
((i++))
done
echo ""
fi

echo "Comment \`/add-rebase-rules\` on this PR to add or update the rules automatically."
exit 1
fi

if [ ${#UNCOVERED[@]} -eq 0 ]; then
echo "All modified files in code/ are covered by rebase rules (or are Che-only additions)."
echo " Covered: $COVERED_COUNT Skipped (lockfiles/Che-only): $SKIPPED_COUNT"
# Default text mode
if [ $TOTAL_ISSUES -eq 0 ]; then
echo "All modified files in code/ are fully covered by rebase rules."
exit 0
fi

echo "**Found ${#UNCOVERED[@]} file(s) modified since the last rebase without rebase rules:**"
echo ""
for f in "${UNCOVERED[@]}"; do
echo " - $f"
done
echo ""
echo "These changes will be lost on the next upstream rebase unless a rule is added."
echo "Found $TOTAL_ISSUES file(s) with rebase rule problems:"
echo ""
echo "To fix, for each file above either:"
echo " 1. Add a .rebase/replace/<path>.json with {\"from\": ..., \"by\": ...} rules"
echo " 2. Add a .rebase/add/<path> or .rebase/override/<path> JSON fragment"
echo " 3. Add an elif branch in resolve_conflicts() in rebase.sh"
if [ ${#MISSING_RULE[@]} -gt 0 ]; then
echo "Missing rules:"
for f in "${MISSING_RULE[@]}"; do echo " - $f"; done
echo ""
fi
if [ ${#INCOMPLETE_RULE[@]} -gt 0 ]; then
echo "Incomplete rules:"
for f in "${INCOMPLETE_RULE[@]}"; do echo " - $f"; done
echo ""
fi
echo "Comment /add-rebase-rules on this PR to fix automatically."
exit 1
5 changes: 3 additions & 2 deletions .github/workflows/check-rebase-rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ jobs:
with:
fetch-depth: 0

- name: Fetch upstream remote
- name: Fetch upstream for rule validation
run: |
UPSTREAM_VERSION=$(grep '^CURRENT_UPSTREAM_VERSION=' rebase.sh | head -1 | sed 's/.*="\(.*\)"/\1/')
git remote add upstream-code https://github.com/microsoft/vscode || true
git fetch upstream-code --no-tags --depth=1
git fetch upstream-code "$UPSTREAM_VERSION" --no-tags --depth=1

- name: Run rebase rules check
id: check
Expand Down
6 changes: 3 additions & 3 deletions code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@
"js-yaml": "^4.1.0"
},
"devDependencies": {
"@playwright/test": "^1.56.1",
"@stylistic/eslint-plugin-ts": "^2.8.0",
"@types/cookie": "^0.3.3",
"@playwright/test": "^1.56.111111111",
"@stylistic/eslint-plugin-ts": "^2.8.1111111",
"@types/cookie": "^0.3.3333",
"@types/debug": "^4.1.5",
"@types/eslint": "^9.6.1",
"@types/gulp-svgmin": "^1.2.1",
Expand Down
Loading