Claude/add auto backup ds6iu #14654
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Safety Guard | |
| on: | |
| pull_request: | |
| branches: [ '**' ] | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| paths-ignore: | |
| - ".cursorrules" | |
| push: | |
| branches: [ '**' ] | |
| paths-ignore: | |
| - ".cursorrules" | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| concurrency: | |
| group: safety-guard-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| safety-guard: | |
| name: "🧯 Dangerous deletes guard" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@v4 | |
| - name: ✅ Ensure .cursorrules exists | |
| run: | | |
| if [ ! -f .cursorrules ]; then | |
| echo "::error::Missing .cursorrules file at repo root" && exit 1 | |
| fi | |
| - name: 🔎 Scan and build report | |
| id: scan | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| pattern='(shutil\\.rmtree|os\\.remove\(|Path\\.unlink\(|rm -rf|rimraf)' | |
| grep -R -nE "$pattern" . --exclude-dir=.git > matches_all.txt || true | |
| excluded_re='^(\./)?docs/|README|Dockerfile|get-pip\\.py|Makefile|build_docs\\.sh' | |
| grep -E "$excluded_re" matches_all.txt > matches_excluded.txt || true | |
| : > flagged.txt | |
| py_files=$(git ls-files '*.py' || true) | |
| if [ -n "${py_files:-}" ]; then | |
| while IFS= read -r f; do | |
| [[ "$f" == docs/* || "$f" == 'get-pip.py' ]] && continue | |
| grep -nE '(shutil\\.rmtree|os\\.remove\(|Path\\.unlink\()' "$f" || true | |
| done < <(printf '%s\n' $py_files) >> flagged.txt | |
| fi | |
| sh_files=$(git ls-files '*.sh' || true) | |
| if [ -n "${sh_files:-}" ]; then | |
| while IFS= read -r f; do | |
| [[ "$f" == docs/* || "$f" == *Makefile* || "$f" == 'build_docs.sh' ]] && continue | |
| grep -nE '(rm -rf|rimraf)' "$f" || true | |
| done < <(printf '%s\n' $sh_files) >> flagged.txt | |
| fi | |
| # Truncate very long lines to keep PR comment readable (e.g., *.map JSON lines) | |
| MAX_COLS=240 | |
| truncate_lines() { # $1=in $2=out | |
| local in_file="$1" out_file="$2" | |
| if [ -s "$in_file" ]; then | |
| awk -v maxc="$MAX_COLS" '{ | |
| if (length($0) > maxc) { | |
| print substr($0, 1, maxc) " … [truncated]"; | |
| } else { print $0 } | |
| }' "$in_file" > "$out_file" | |
| else | |
| : > "$out_file" | |
| fi | |
| } | |
| truncate_lines matches_excluded.txt matches_excluded_short.txt | |
| truncate_lines flagged.txt flagged_short.txt | |
| num_flagged=$(grep -c ':' flagged.txt 2>/dev/null || echo 0) | |
| num_excluded=$(grep -c ':' matches_excluded.txt 2>/dev/null || echo 0) | |
| num_all=$(grep -c ':' matches_all.txt 2>/dev/null || echo 0) | |
| { | |
| echo "## 🧯 Dangerous deletes guard report" | |
| echo | |
| echo "Policy: see .cursorrules — dangerous deletions are blocked unless wrapped safely." | |
| echo | |
| echo "Summary:" | |
| echo "- Flagged findings (blocking): ${num_flagged}" | |
| echo "- Excluded matches (not blocking): ${num_excluded}" | |
| echo "- Total matches (all files): ${num_all}" | |
| echo | |
| echo "Flagged findings (file:line:snippet):" | |
| if [ -s flagged_short.txt ]; then | |
| echo '```' | |
| cat flagged_short.txt | |
| echo '```' | |
| else | |
| echo "(none)" | |
| fi | |
| echo | |
| echo "<details><summary>Excluded matches (by path pattern)</summary>" | |
| echo | |
| echo '```' | |
| cat matches_excluded_short.txt 2>/dev/null || true | |
| echo '```' | |
| echo "</details>" | |
| } > scan_report.md | |
| if [ "$num_flagged" -gt 0 ]; then | |
| echo "has_violations=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_violations=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: 💬 Post PR report comment | |
| if: always() && github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- safety-guard-report -->'; | |
| let body = ''; | |
| try { body = fs.readFileSync('scan_report.md', 'utf8'); } catch (e) { body = 'Report missing.' } | |
| // Truncate to stay under GitHub's 65,536 chars issue comment limit | |
| const MAX = 65000; | |
| const suffix = '\n\n[...truncated, see workflow logs for full report]'; | |
| let safeBody = body; | |
| if (typeof body === 'string' && body.length > MAX) { | |
| safeBody = body.slice(0, MAX - suffix.length) + suffix; | |
| } | |
| const full = `${marker}\n${safeBody}`; | |
| const pr = context.payload.pull_request?.number; | |
| if (!pr) { core.info('Not a PR event; skip comment'); return; } | |
| const { owner, repo } = context.repo; | |
| const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: pr, per_page: 100 }); | |
| const existing = comments.find(c => (c.user?.type === 'Bot' || (c.user?.login || '').includes('[bot]')) && (c.body || '').includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body: full }); | |
| } else { | |
| await github.rest.issues.createComment({ owner, repo, issue_number: pr, body: full }); | |
| } | |
| - name: ❌ Fail if violations found | |
| if: steps.scan.outputs.has_violations == 'true' | |
| run: | | |
| echo "Blocking merge: flagged dangerous deletion calls found. See report above." >&2 | |
| exit 1 | |