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