Skip to content

Commit 13803ed

Browse files
committed
ci: add ceo-audit.yml — SOTA 47-gate audit on PR/push
1 parent 4e0e1aa commit 13803ed

1 file changed

Lines changed: 226 additions & 0 deletions

File tree

.github/workflows/ceo-audit.yml

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# Purpose: CEO Audit — SOTA repository review (47 gates, 8 axes)
2+
# Docs: https://github.com/OpenSIN-Code/SIN-Code-Bundle/tree/main/src/sin_code_bundle/skills/ceo-audit
3+
#
4+
# Runs the full CEO Audit on every push and PR. Posts a Markdown
5+
# comment on the PR with the grade, top 3 risks, and a link to the
6+
# full report. Fails if grade < B (configurable via --grade flag).
7+
#
8+
# Required secrets: none (uses built-in GITHUB_TOKEN)
9+
# Optional inputs: profile (default: QUICK), grade (default: B)
10+
11+
name: ceo-audit
12+
13+
on:
14+
# NUR main/master (Branches sind verboten — siehe globale AGENTS.md).
15+
# PRs sind weiterhin willkommen (last line of defense wenn doch einer entsteht).
16+
push:
17+
branches: [main, master]
18+
pull_request:
19+
branches: [main, master]
20+
workflow_dispatch:
21+
inputs:
22+
profile:
23+
description: 'Audit profile: QUICK | RELEASE | SECURITY | FULL'
24+
required: false
25+
default: 'QUICK'
26+
grade:
27+
description: 'Minimum grade to pass: A | B | C'
28+
required: false
29+
default: 'B'
30+
31+
permissions:
32+
contents: read
33+
pull-requests: write
34+
checks: write
35+
36+
jobs:
37+
ceo-audit:
38+
name: CEO Audit (${{ inputs.profile || 'QUICK' }}, grade≥${{ inputs.grade || 'B' }})
39+
runs-on: ubuntu-latest
40+
timeout-minutes: 15
41+
env:
42+
AUDIT_PROFILE: ${{ inputs.profile || 'QUICK' }}
43+
AUDIT_GRADE: ${{ inputs.grade || 'B' }}
44+
AUDIT_REPO: ${{ github.workspace }}
45+
AUDIT_RUN_ID: ${{ github.run_id }}
46+
AUDIT_SHA: ${{ github.sha }}
47+
CEO_AUDIT_OUTPUT: ${{ github.workspace }}/ceo-audit-output
48+
# The bundle's audit.sh defaults to $HOME/ceo-audits; we override to
49+
# match the workflow's expected ceo-audit-output/ path so score.json
50+
# lands where the next steps (upload-sarif, comment) expect it.
51+
steps:
52+
- name: Checkout
53+
uses: actions/checkout@v4
54+
with:
55+
fetch-depth: 0 # full history for regression detection
56+
57+
- name: Setup Python
58+
uses: actions/setup-python@v5
59+
with:
60+
python-version: '3.12'
61+
cache: 'pip'
62+
63+
- name: Install SIN-Code Bundle (with ceo-audit skill)
64+
# Try PyPI first, fall back to GitHub (bundle is not yet on PyPI).
65+
# Once published: pip install "sin-code-bundle[ceo-audit,dev]"
66+
run: |
67+
pip install "sin-code-bundle[ceo-audit,dev]" || \
68+
pip install "sin-code-bundle[ceo-audit,dev] @ git+https://github.com/OpenSIN-Code/SIN-Code-Bundle.git@v0.4.4"
69+
70+
- name: Install ceo-audit skill
71+
run: |
72+
# sin-code-bundle does not yet ship the skill scripts.
73+
# Clone the SSOT (Infra-SIN-OpenCode-Stack) to get audit.sh + axis scripts.
74+
git clone --depth 1 --branch main https://github.com/OpenSIN-Code/Infra-SIN-OpenCode-Stack.git ${{ github.workspace }}/infra
75+
mkdir -p ~/.config/opencode/skills/ceo-audit
76+
cp -r ${{ github.workspace }}/infra/skills/ceo-audit/scripts ~/.config/opencode/skills/ceo-audit/
77+
cp -r ${{ github.workspace }}/infra/skills/ceo-audit/lib ~/.config/opencode/skills/ceo-audit/
78+
chmod +x ~/.config/opencode/skills/ceo-audit/scripts/audit.sh
79+
ls ~/.config/opencode/skills/ceo-audit/scripts/audit.sh
80+
81+
- name: Locate audit.sh on PATH
82+
id: locate
83+
run: |
84+
# After 'pip install sin-code-bundle[ceo-audit,dev]', audit.sh is
85+
# shipped at <site-packages>/sin_code_bundle/resources/ceo-audit/scripts/audit.sh.
86+
# We also accept a git-clone of the skill to ~/.config/opencode/skills/.
87+
SITE_PKG_SCRIPT=$(python3 -c "import sin_code_bundle, os; root=os.path.dirname(sin_code_bundle.__file__); p=os.path.join(root,'resources','ceo-audit','scripts','audit.sh'); print(p if os.path.isfile(p) else '')" 2>/dev/null)
88+
if [ -n "$SITE_PKG_SCRIPT" ] && [ -f "$SITE_PKG_SCRIPT" ]; then
89+
echo "script=$SITE_PKG_SCRIPT" >> $GITHUB_OUTPUT
90+
elif [ -f ~/.config/opencode/skills/ceo-audit/scripts/audit.sh ]; then
91+
echo "script=~/.config/opencode/skills/ceo-audit/scripts/audit.sh" >> $GITHUB_OUTPUT
92+
else
93+
echo '::error::Could not locate audit.sh (not in site-packages, not on disk)'
94+
exit 1
95+
fi
96+
echo "Located audit script: $SITE_PKG_SCRIPT"
97+
98+
- name: Run CEO Audit
99+
id: audit
100+
run: |
101+
mkdir -p ceo-audit-output
102+
# Run audit; capture exit code (allow failure so we can still post the report)
103+
set +e
104+
${{ steps.locate.outputs.script }} \
105+
"$AUDIT_REPO" \
106+
--profile="$AUDIT_PROFILE" \
107+
--grade="$AUDIT_GRADE" \
108+
--output="$AUDIT_REPO/ceo-audit-output" \
109+
--json 2>&1 | tee ceo-audit-output/console.log
110+
AUDIT_EXIT=$?
111+
set -e
112+
echo "audit_exit_code=$AUDIT_EXIT" >> $GITHUB_OUTPUT
113+
# Don't fail the step yet — we want to always upload the report + post the comment
114+
115+
- name: Upload audit artifacts
116+
if: always()
117+
uses: actions/upload-artifact@v4
118+
with:
119+
name: ceo-audit-${{ github.run_id }}
120+
path: ceo-audit-output/
121+
retention-days: 30
122+
if-no-files-found: warn
123+
124+
- name: Extract grade from score.json
125+
id: grade
126+
if: always()
127+
run: |
128+
SCORE_FILE=$(find ceo-audit-output -name 'score.json' | head -1)
129+
if [ -z "$SCORE_FILE" ]; then
130+
echo "::error::CEO Audit did not produce score.json"
131+
echo "grade=unknown" >> $GITHUB_OUTPUT
132+
echo "score=0" >> $GITHUB_OUTPUT
133+
echo "verdict=Audit failed" >> $GITHUB_OUTPUT
134+
exit 0
135+
fi
136+
GRADE=$(jq -r '.grade // "?"' "$SCORE_FILE")
137+
SCORE=$(jq -r '.score // 0' "$SCORE_FILE")
138+
CRITICAL=$(jq -r '.critical // 0' "$SCORE_FILE")
139+
HIGH=$(jq -r '.high // 0' "$SCORE_FILE")
140+
echo "grade=$GRADE" >> $GITHUB_OUTPUT
141+
echo "score=$SCORE" >> $GITHUB_OUTPUT
142+
echo "critical=$CRITICAL" >> $GITHUB_OUTPUT
143+
echo "high=$HIGH" >> $GITHUB_OUTPUT
144+
echo "::notice::CEO Audit: $GRADE ($SCORE/100) | critical=$CRITICAL high=$HIGH"
145+
146+
- name: Post PR comment
147+
if: github.event_name == 'pull_request' && always()
148+
uses: marocchino/sticky-pull-request-comment@v2
149+
with:
150+
header: ceo-audit
151+
message: |
152+
## 🏆 CEO Audit — ${{ steps.grade.outputs.grade || '?' }} (${{ steps.grade.outputs.score || '0' }}/100)
153+
154+
| Metric | Value |
155+
|--------|-------|
156+
| **Grade** | **${{ steps.grade.outputs.grade || '?' }}** |
157+
| **Score** | **${{ steps.grade.outputs.score || '0' }}/100** |
158+
| **Critical findings** | ${{ steps.grade.outputs.critical || '0' }} |
159+
| **High findings** | ${{ steps.grade.outputs.high || '0' }} |
160+
| **Profile** | `${{ env.AUDIT_PROFILE }}` |
161+
| **Min grade gate** | ${{ env.AUDIT_GRADE }} |
162+
163+
📥 [Download full report (Markdown)](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts)
164+
📊 [Download SARIF (for Code Scanning)](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts)
165+
166+
> Run `${{ env.AUDIT_PROFILE == 'FULL' && '~/.config/opencode/skills/ceo-audit/scripts/audit.sh . --profile=FULL' || '~/.config/opencode/skills/ceo-audit/scripts/audit.sh . --profile=QUICK' }}` locally to reproduce.
167+
168+
- name: Post official audit comment (SIN-GitHub-Issues App)
169+
if: github.event_name == 'pull_request' && always()
170+
# Token resolution chain (highest priority first):
171+
# 1. SIN_GITHUB_INSTALLATION_TOKEN (org secret, App identity, public repos only)
172+
# 2. SIN_GITHUB_FALLBACK_TOKEN (repo secret, PAT — works on ALL repos incl. private)
173+
# 3. GITHUB_TOKEN (built-in, Action identity, always present)
174+
# Resolution happens inside post_audit_pr.py via github_app.get_token().
175+
# If ALL tokens are missing, the step fails but continue-on-error prevents
176+
# the workflow from blocking on App issues.
177+
continue-on-error: true
178+
env:
179+
PYTHONPATH: ${{ github.workspace }}/infra/skills/ceo-audit/lib
180+
SIN_GITHUB_APP_CLIENT_ID: Iv23livllaHIBTdQdyhY
181+
# Chain of GitHub tokens (post_audit_pr.py picks the first available).
182+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
183+
SIN_GITHUB_FALLBACK_TOKEN: ${{ secrets.SIN_GITHUB_FALLBACK_TOKEN }}
184+
run: |
185+
# post_audit_pr.py lives in the cloned Infra repo (see 'Install ceo-audit skill' step)
186+
# score.json is written by audit.sh to ~/ceo-audits/<repo>-ceo-audit-<runid>/score.json
187+
# We search both ceo-audit-output/ and ~/ceo-audits/ to be robust.
188+
SCORE_FILE=$(find $HOME/ceo-audits ceo-audit-output -name 'score.json' 2>/dev/null | head -1)
189+
if [ -z "$SCORE_FILE" ]; then
190+
echo "::warning::No score.json found — skipping App commenter (Action comment above still posts)"
191+
exit 0
192+
fi
193+
echo "Using score.json: $SCORE_FILE"
194+
python3 ${{ github.workspace }}/infra/skills/ceo-audit/scripts/post_audit_pr.py \
195+
--repo ${{ github.repository }} \
196+
--pr ${{ github.event.pull_request.number }} \
197+
--score-json "$SCORE_FILE" \
198+
--artifact-url ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} \
199+
--run-id ${{ github.run_id }}
200+
201+
- name: Fail if grade below gate
202+
if: github.event_name == 'pull_request'
203+
run: |
204+
GRADE="${{ steps.grade.outputs.grade }}"
205+
GRADE_NUM="${{ steps.grade.outputs.score }}"
206+
GATE="${{ env.AUDIT_GRADE }}"
207+
case "$GATE" in
208+
A) MIN=85 ;;
209+
B) MIN=70 ;;
210+
C) MIN=55 ;;
211+
*) MIN=0 ;;
212+
esac
213+
# Allow only A and B by default
214+
if (( $(echo "$GRADE_NUM < $MIN" | bc -l) )); then
215+
echo "::error::Grade $GRADE ($GRADE_NUM) below gate $GATE (need ≥$MIN)"
216+
exit 1
217+
fi
218+
echo "::notice::Grade gate passed: $GRADE ($GRADE_NUM) ≥ $GATE ($MIN)"
219+
220+
- name: Upload SARIF to Code Scanning
221+
if: always()
222+
uses: github/codeql-action/upload-sarif@v3
223+
with:
224+
sarif_file: ${{ github.workspace }}/ceo-audit-output/report.sarif
225+
category: ceo-audit
226+
continue-on-error: true

0 commit comments

Comments
 (0)