|
1 | | -# SPDX-License-Identifier: MPL-2.0 |
2 | | -# This workflow is managed by gh actions-lock. |
3 | 1 | # This workflow is managed by gh actions-lock. |
4 | | -# rhodibot.yml — Automated RSR compliance enforcement |
| 2 | +# SPDX-License-Identifier: MPL-2.0 |
| 3 | +# rhodibot.yml — RSR compliance CANARY (report-only) |
5 | 4 | # |
6 | | -# Reads root-hygiene rules and auto-fixes what it can: |
7 | | -# - Delete banned files (AI.djot, duplicate CONTRIBUTING.adoc, stale snapshots) |
8 | | -# - Rename misnamed files (AI.a2ml → 0-AI-MANIFEST.a2ml) |
9 | | -# - Fix SPDX headers (AGPL → MPL-2.0 in dotfiles) |
10 | | -# - Create missing required files (SECURITY.md, CONTRIBUTING.md) |
11 | | -# - Report unfixable issues as PR comments |
| 5 | +# Rhodibot does NOT mutate this repository. It never deletes, renames, |
| 6 | +# rewrites SPDX headers, creates files, or opens PRs. Instead it DETECTS |
| 7 | +# what an auto-fixer would have changed and reports it. |
12 | 8 | # |
13 | | -# Runs weekly and on Hypatia scan completion. |
14 | | - |
15 | | -name: "🤖 Rhodibot — RSR Auto-Fix" |
| 9 | +# Design intent (owner): if rhodibot "feels the desire to edit" — i.e. it |
| 10 | +# detects something it considers non-compliant — that is itself a MAJOR |
| 11 | +# WARNING. Either the repo has drifted, OR rhodibot's own rules have |
| 12 | +# diverged from the normative style it is meant to enforce. Both warrant |
| 13 | +# a human look, so the canary FAILS the run when it finds would-mutate |
| 14 | +# drift. Dangerous-pattern hits are advisory warnings only. |
| 15 | +# |
| 16 | +# Licence note: SPDX/licence drift is reported for MANUAL, owner-only |
| 17 | +# correction. Rhodibot must never edit a licence header (estate directive). |
16 | 18 |
|
| 19 | +name: "\U0001F916 Rhodibot — RSR Compliance Canary" |
17 | 20 | on: |
18 | 21 | schedule: |
19 | | - - cron: '0 6 * * 1' # Every Monday at 06:00 UTC |
20 | | - workflow_dispatch: # Manual trigger |
21 | | - workflow_run: |
22 | | - workflows: ["Hypatia Neurosymbolic Analysis"] |
23 | | - types: [completed] |
| 22 | + - cron: '0 6 * * 1' # Every Monday at 06:00 UTC |
| 23 | + workflow_dispatch: # Manual trigger |
24 | 24 |
|
25 | | -permissions: |
26 | | - contents: write |
27 | | - pull-requests: write |
| 25 | +concurrency: |
| 26 | + group: ${{ github.workflow }}-${{ github.ref }} |
| 27 | + cancel-in-progress: true |
28 | 28 |
|
| 29 | +permissions: |
| 30 | + contents: read |
29 | 31 | jobs: |
30 | | - rhodibot: |
| 32 | + canary: |
31 | 33 | runs-on: ubuntu-latest |
| 34 | + timeout-minutes: 15 |
32 | 35 | steps: |
33 | 36 | - name: Checkout |
34 | 37 | uses: actions/checkout@v7.0.1 |
35 | 38 | with: |
36 | 39 | fetch-depth: 1 |
37 | | - |
38 | | - - name: Rhodibot — Scan and Fix |
39 | | - id: fix |
| 40 | + - name: Rhodibot — detect drift (no mutations) |
40 | 41 | run: | |
41 | | - set -euo pipefail |
42 | | - FIXES="" |
43 | | - ISSUES="" |
44 | | - CHANGED=false |
| 42 | + set -uo pipefail |
| 43 | + DRIFT=0 |
| 44 | + warn() { echo "::warning title=Rhodibot canary::$*"; DRIFT=$((DRIFT+1)); } |
| 45 | + note() { echo "::warning title=Rhodibot advisory::$*"; } |
45 | 46 |
|
46 | | - # --- 1. Delete banned files --- |
47 | | - for pattern in "AI.djot" "NEXT_STEPS.md" "TODO.md" "NOTES.md" "TASKS.md"; do |
48 | | - if [ -f "$pattern" ]; then |
49 | | - rm "$pattern" |
50 | | - FIXES="$FIXES\n- Deleted \`$pattern\` (superseded)" |
51 | | - CHANGED=true |
52 | | - fi |
53 | | - done |
| 47 | + echo "## 🤖 Rhodibot canary — report only (no edits made)" >> "$GITHUB_STEP_SUMMARY" |
54 | 48 |
|
55 | | - # Delete stale snapshot files |
| 49 | + # --- would-DELETE: banned files --- |
| 50 | + for f in AI.djot NEXT_STEPS.md TODO.md NOTES.md TASKS.md; do |
| 51 | + [ -f "$f" ] && warn "banned file present: $f (an auto-fixer would delete it)" |
| 52 | + done |
| 53 | + # would-DELETE: stale snapshots |
56 | 54 | for f in *-STATUS-*.md *-COMPLETION-*.md *-COMPLETE.md *-VERIFIED-*.md; do |
57 | | - if [ -f "$f" ]; then |
58 | | - rm "$f" |
59 | | - FIXES="$FIXES\n- Deleted stale snapshot \`$f\`" |
60 | | - CHANGED=true |
61 | | - fi |
| 55 | + [ -f "$f" ] && warn "stale snapshot present: $f (would be deleted)" |
62 | 56 | done |
63 | | -
|
64 | | - # --- 2. Rename misnamed files --- |
| 57 | + # would-RENAME: legacy manifest name |
65 | 58 | if [ -f "AI.a2ml" ] && [ ! -f "0-AI-MANIFEST.a2ml" ]; then |
66 | | - mv AI.a2ml 0-AI-MANIFEST.a2ml |
67 | | - FIXES="$FIXES\n- Renamed \`AI.a2ml\` → \`0-AI-MANIFEST.a2ml\`" |
68 | | - CHANGED=true |
| 59 | + warn "AI.a2ml present without 0-AI-MANIFEST.a2ml (would be renamed)" |
69 | 60 | fi |
70 | | -
|
71 | | - # --- 3. Delete duplicate format files --- |
72 | | - if [ -f "CONTRIBUTING.md" ] && [ -f "CONTRIBUTING.adoc" ]; then |
73 | | - rm CONTRIBUTING.adoc |
74 | | - FIXES="$FIXES\n- Deleted duplicate \`CONTRIBUTING.adoc\` (keeping .md for GitHub)" |
75 | | - CHANGED=true |
| 61 | + # would-DELETE: duplicate community files |
| 62 | + [ -f "CONTRIBUTING.md" ] && [ -f "CONTRIBUTING.adoc" ] && warn "duplicate CONTRIBUTING.md + CONTRIBUTING.adoc (one would be removed)" |
| 63 | + if [ -f "README.md" ] && [ -f "README.adoc" ] && [ "$(wc -l < README.md)" -lt 5 ]; then |
| 64 | + warn "stub README.md alongside README.adoc (would be removed)" |
76 | 65 | fi |
77 | | -
|
78 | | - if [ -f "README.md" ] && [ -f "README.adoc" ]; then |
79 | | - # Only delete README.md if it's a stub (<5 lines) |
80 | | - lines=$(wc -l < README.md) |
81 | | - if [ "$lines" -lt 5 ]; then |
82 | | - rm README.md |
83 | | - FIXES="$FIXES\n- Deleted stub \`README.md\` (keeping .adoc)" |
84 | | - CHANGED=true |
85 | | - fi |
86 | | - fi |
87 | | -
|
88 | | - # --- 4. Fix SPDX headers in dotfiles --- |
| 66 | + # SPDX drift — MANUAL owner-only fix, never auto-edited |
89 | 67 | for dotfile in .gitignore .gitattributes .editorconfig; do |
90 | | - if [ -f "$dotfile" ] && grep -q "AGPL-3.0" "$dotfile" 2>/dev/null; then |
91 | | - sed -i 's/AGPL-3.0-or-later/MPL-2.0/g; s/AGPL-3.0/MPL-2.0/g' "$dotfile" |
92 | | - FIXES="$FIXES\n- Fixed SPDX header in \`$dotfile\` (AGPL → MPL-2.0)" |
93 | | - CHANGED=true |
| 68 | + if [ -f "$dotfile" ] && grep "AGPL-3.0" "$dotfile" 2>/dev/null | grep -v "AGPL-3.0-or-later" | grep -q .; then |
| 69 | + warn "$dotfile carries an AGPL-3.0 SPDX header; estate policy is MPL-2.0 — fix MANUALLY (owner-only, never auto-edited)" |
94 | 70 | fi |
95 | 71 | done |
96 | | -
|
97 | | - # --- 5. Create missing required files --- |
98 | | - if [ ! -f "SECURITY.md" ]; then |
99 | | - cat > SECURITY.md << 'SECEOF' |
100 | | - <!-- SPDX-License-Identifier: MPL-2.0 --> |
101 | | - # Security Policy |
102 | | -
|
103 | | - ## Reporting a Vulnerability |
104 | | -
|
105 | | - **Email:** j.d.a.jewell@open.ac.uk |
106 | | -
|
107 | | - **Response timeline:** |
108 | | - - Acknowledgement within 48 hours |
109 | | - - Initial assessment within 7 days |
110 | | - - Fix or mitigation within 90 days |
111 | | -
|
112 | | - **Safe harbour:** We will not pursue legal action against security researchers who follow responsible disclosure. |
113 | | - SECEOF |
114 | | - FIXES="$FIXES\n- Created missing \`SECURITY.md\`" |
115 | | - CHANGED=true |
116 | | - fi |
117 | | -
|
118 | | - if [ ! -f "CONTRIBUTING.md" ]; then |
119 | | - cat > CONTRIBUTING.md << 'CONTEOF' |
120 | | - <!-- SPDX-License-Identifier: MPL-2.0 --> |
121 | | - # Contributing |
122 | | -
|
123 | | - 1. Fork the repository |
124 | | - 2. Create a feature branch |
125 | | - 3. Ensure SPDX headers on all files |
126 | | - 4. Submit a pull request |
127 | | -
|
128 | | - **Author:** Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
129 | | - CONTEOF |
130 | | - FIXES="$FIXES\n- Created missing \`CONTRIBUTING.md\`" |
131 | | - CHANGED=true |
132 | | - fi |
133 | | -
|
134 | | - # --- 6. Check for issues we can't auto-fix --- |
135 | | - if [ ! -f "0-AI-MANIFEST.a2ml" ] && [ ! -f "AI.a2ml" ]; then |
136 | | - ISSUES="$ISSUES\n- Missing AI manifest (0-AI-MANIFEST.a2ml)" |
137 | | - fi |
138 | | -
|
139 | | - if [ ! -f "LICENSE" ] && [ ! -f "LICENSE.md" ] && [ ! -f "LICENSE.txt" ]; then |
140 | | - ISSUES="$ISSUES\n- Missing LICENSE file" |
141 | | - fi |
142 | | -
|
143 | | - if [ ! -f "README.adoc" ] && [ ! -f "README.md" ]; then |
144 | | - ISSUES="$ISSUES\n- Missing README" |
145 | | - fi |
146 | | -
|
147 | | - # Check for third-party fork (skip SPDX enforcement) |
148 | | - if [ -f "LICENSE" ] && grep -q "multiple licenses\|LGPL\|Apache" LICENSE 2>/dev/null; then |
149 | | - echo "FORK=true" >> $GITHUB_OUTPUT |
150 | | - fi |
151 | | -
|
152 | | - # --- 7. Check dangerous patterns --- |
153 | | - DANGEROUS="" |
154 | | - for pattern in "believe_me" "assert_total" "Admitted" "sorry" "unsafeCoerce" "Obj.magic"; do |
155 | | - count=$(grep -r "$pattern" --include='*.idr' --include='*.v' --include='*.lean' --include='*.hs' --include='*.ml' --include='*.res' . 2>/dev/null | grep -v node_modules | wc -l || echo 0) |
156 | | - if [ "$count" -gt 0 ]; then |
157 | | - DANGEROUS="$DANGEROUS\n- \`$pattern\`: $count occurrences" |
158 | | - fi |
| 72 | + # would-CREATE: missing required files |
| 73 | + [ -f "SECURITY.md" ] || [ -f ".github/SECURITY.md" ] || warn "no SECURITY.md (would be created)" |
| 74 | + [ -f "CONTRIBUTING.md" ] || [ -f ".github/CONTRIBUTING.md" ] || warn "no CONTRIBUTING.md (would be created)" |
| 75 | +
|
| 76 | + # --- unfixable compliance gaps (also drift) --- |
| 77 | + [ -f "0-AI-MANIFEST.a2ml" ] || [ -f "AI.a2ml" ] || warn "missing AI manifest (0-AI-MANIFEST.a2ml)" |
| 78 | + [ -f "LICENSE" ] || [ -f "LICENSE.md" ] || [ -f "LICENSE.txt" ] || warn "missing LICENSE file" |
| 79 | + [ -f "README.adoc" ] || [ -f "README.md" ] || warn "missing README" |
| 80 | +
|
| 81 | + # --- advisory only: dangerous verification-bypass patterns --- |
| 82 | + for pattern in believe_me assert_total Admitted sorry unsafeCoerce Obj.magic; do |
| 83 | + count=$(grep -rl "$pattern" --include='*.idr' --include='*.v' --include='*.lean' --include='*.hs' --include='*.ml' --include='*.res' . 2>/dev/null | grep -v node_modules | wc -l || true) |
| 84 | + [ "$count" -gt 0 ] && note "verification-bypass pattern '$pattern' in $count file(s) (advisory)" |
159 | 85 | done |
160 | 86 |
|
161 | | - # Output results |
162 | | - echo "CHANGED=$CHANGED" >> $GITHUB_OUTPUT |
163 | | - { |
164 | | - echo "FIXES<<EOF" |
165 | | - echo -e "$FIXES" |
166 | | - echo "EOF" |
167 | | - } >> $GITHUB_OUTPUT |
168 | | - { |
169 | | - echo "ISSUES<<EOF" |
170 | | - echo -e "$ISSUES" |
171 | | - echo "EOF" |
172 | | - } >> $GITHUB_OUTPUT |
173 | | - { |
174 | | - echo "DANGEROUS<<EOF" |
175 | | - echo -e "$DANGEROUS" |
176 | | - echo "EOF" |
177 | | - } >> $GITHUB_OUTPUT |
178 | | -
|
179 | | - - name: Create PR with fixes |
180 | | - if: steps.fix.outputs.CHANGED == 'true' |
181 | | - run: | |
182 | | - git config user.name "rhodibot" |
183 | | - git config user.email "rhodibot@hyperpolymath.dev" |
184 | | - BRANCH="rhodibot/rsr-compliance-$(date +%Y%m%d)" |
185 | | - git checkout -b "$BRANCH" |
186 | | - git add -A |
187 | | - git commit -m "fix(rhodibot): automated RSR compliance fixes |
188 | | -
|
189 | | - ${{ steps.fix.outputs.FIXES }} |
190 | | -
|
191 | | - Co-Authored-By: rhodibot <rhodibot@hyperpolymath.dev>" |
192 | | -
|
193 | | - git push origin "$BRANCH" |
194 | | -
|
195 | | - BODY="## 🤖 Rhodibot — RSR Compliance Fixes |
196 | | -
|
197 | | - ### Changes Made |
198 | | - ${{ steps.fix.outputs.FIXES }} |
199 | | - " |
200 | | -
|
201 | | - if [ -n "${{ steps.fix.outputs.ISSUES }}" ]; then |
202 | | - BODY="$BODY |
203 | | - ### Issues Found (manual fix needed) |
204 | | - ${{ steps.fix.outputs.ISSUES }} |
205 | | - " |
206 | | - fi |
207 | | -
|
208 | | - if [ -n "${{ steps.fix.outputs.DANGEROUS }}" ]; then |
209 | | - BODY="$BODY |
210 | | - ### ⚠️ Dangerous Patterns Detected |
211 | | - ${{ steps.fix.outputs.DANGEROUS }} |
212 | | -
|
213 | | - _These bypass formal verification. See \`proven\` repo for alternatives._ |
214 | | - " |
215 | | - fi |
216 | | -
|
217 | | - gh pr create \ |
218 | | - --title "🤖 Rhodibot: RSR compliance fixes" \ |
219 | | - --body "$BODY" \ |
220 | | - --base main \ |
221 | | - --head "$BRANCH" |
222 | | - env: |
223 | | - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
224 | | - |
225 | | - - name: Report (no changes needed) |
226 | | - if: steps.fix.outputs.CHANGED != 'true' |
227 | | - run: | |
228 | | - echo "✅ Repository is RSR-compliant. No fixes needed." |
229 | | - if [ -n "${{ steps.fix.outputs.ISSUES }}" ]; then |
230 | | - echo "⚠️ Issues found (manual fix needed):" |
231 | | - echo -e "${{ steps.fix.outputs.ISSUES }}" |
232 | | - fi |
233 | | - if [ -n "${{ steps.fix.outputs.DANGEROUS }}" ]; then |
234 | | - echo "⚠️ Dangerous patterns:" |
235 | | - echo -e "${{ steps.fix.outputs.DANGEROUS }}" |
| 87 | + echo "" >> "$GITHUB_STEP_SUMMARY" |
| 88 | + if [ "$DRIFT" -gt 0 ]; then |
| 89 | + echo "🔴 **Canary tripped: $DRIFT would-mutate finding(s).** Either the repo drifted or rhodibot's rules diverged from the norm — investigate (no edits were made)." >> "$GITHUB_STEP_SUMMARY" |
| 90 | + echo "::error title=Rhodibot canary::$DRIFT would-mutate finding(s) detected — rhodibot wants to edit. Investigate; nothing was changed." |
| 91 | + exit 1 |
236 | 92 | fi |
| 93 | + echo "✅ Canary clean — rhodibot has no desire to edit. Repository matches the norm." >> "$GITHUB_STEP_SUMMARY" |
| 94 | + echo "✅ Rhodibot canary clean — no drift, no mutations." |
0 commit comments