Skip to content

Commit 2b3c435

Browse files
thegdsksglinr
andcommitted
feat(audit): add --badge flag and GitHub Action for CI/CD
- Badge CLI: --badge outputs shields.io badge snippets (static + dynamic) - GitHub Action: marketplace-ready composite action with job summary, PR comments, threshold gating, badge output, version pinning - 12 new badge tests, all 323 tests passing Co-Authored-By: Glinr <bot@glincker.com>
1 parent 079359b commit 2b3c435

File tree

10 files changed

+584
-0
lines changed

10 files changed

+584
-0
lines changed

actions/geo-audit-action/README.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# GEO Audit — GitHub Action
2+
3+
Audit any website's AI-readiness score in your CI/CD pipeline. Scores 20 rules across discoverability, structured data, content quality, and technical readiness. Returns a 0-100 score and A-F grade.
4+
5+
## Quick Start
6+
7+
```yaml
8+
- uses: GLINCKER/geo-audit-action@v1
9+
with:
10+
url: https://your-site.com
11+
```
12+
13+
## Inputs
14+
15+
| Input | Required | Default | Description |
16+
|-------|----------|---------|-------------|
17+
| `url` | **Yes** | — | URL to audit |
18+
| `fail-under` | No | `0` | Fail the step if score is below this threshold |
19+
| `comment` | No | `false` | Post score as a PR comment (updates existing comment) |
20+
| `badge` | No | `false` | Include badge markdown snippet in job summary |
21+
| `timeout` | No | `15000` | Fetch timeout in milliseconds |
22+
| `version` | No | `latest` | Pin to a specific `@glincker/geo-audit` version |
23+
24+
## Outputs
25+
26+
| Output | Description | Example |
27+
|--------|-------------|---------|
28+
| `score` | Numeric score (0-100) | `85` |
29+
| `grade` | Letter grade | `A` |
30+
| `badge` | Static shields.io badge URL | `https://img.shields.io/badge/...` |
31+
| `result` | Full JSON audit result | `{"url":"...","score":85,...}` |
32+
33+
## Features
34+
35+
- **Job Summary** — Rich markdown report in the Actions tab (always on)
36+
- **PR Comments** — Auto-posts score on pull requests, updates existing comment (no spam)
37+
- **Badge Output** — Generates shields.io badge URL as an output
38+
- **Threshold Gate** — Fail CI if score drops below a minimum
39+
- **Version Pinning** — Lock to a specific audit version for reproducible builds
40+
41+
## Examples
42+
43+
### Basic PR Check with Threshold
44+
45+
```yaml
46+
name: AI-Readiness
47+
on: pull_request
48+
49+
jobs:
50+
audit:
51+
runs-on: ubuntu-latest
52+
steps:
53+
- uses: GLINCKER/geo-audit-action@v1
54+
with:
55+
url: https://your-site.com
56+
fail-under: 60
57+
```
58+
59+
### PR Comment with Badge
60+
61+
```yaml
62+
name: AI-Readiness Report
63+
on: pull_request
64+
65+
permissions:
66+
pull-requests: write
67+
68+
jobs:
69+
audit:
70+
runs-on: ubuntu-latest
71+
steps:
72+
- uses: GLINCKER/geo-audit-action@v1
73+
with:
74+
url: https://your-site.com
75+
comment: true
76+
badge: true
77+
```
78+
79+
### Audit Deploy Preview
80+
81+
```yaml
82+
name: Audit Preview
83+
on:
84+
deployment_status:
85+
types: [success]
86+
87+
jobs:
88+
audit:
89+
if: github.event.deployment_status.state == 'success'
90+
runs-on: ubuntu-latest
91+
steps:
92+
- uses: GLINCKER/geo-audit-action@v1
93+
with:
94+
url: ${{ github.event.deployment_status.target_url }}
95+
fail-under: 50
96+
```
97+
98+
### Weekly Scheduled Audit
99+
100+
```yaml
101+
name: Weekly AI-Readiness
102+
on:
103+
schedule:
104+
- cron: '0 9 * * 1'
105+
106+
jobs:
107+
audit:
108+
runs-on: ubuntu-latest
109+
steps:
110+
- uses: GLINCKER/geo-audit-action@v1
111+
id: geo
112+
with:
113+
url: https://your-site.com
114+
115+
- name: Alert if score drops
116+
if: steps.geo.outputs.score < 70
117+
run: echo "::warning::AI-Readiness score dropped to ${{ steps.geo.outputs.score }}"
118+
```
119+
120+
### Use Score in Downstream Steps
121+
122+
```yaml
123+
jobs:
124+
audit:
125+
runs-on: ubuntu-latest
126+
steps:
127+
- uses: GLINCKER/geo-audit-action@v1
128+
id: geo
129+
with:
130+
url: https://your-site.com
131+
132+
- name: Use results
133+
run: |
134+
echo "Score: ${{ steps.geo.outputs.score }}"
135+
echo "Grade: ${{ steps.geo.outputs.grade }}"
136+
echo "Badge: ${{ steps.geo.outputs.badge }}"
137+
```
138+
139+
## Grading Scale
140+
141+
| Grade | Score | Color |
142+
|-------|-------|-------|
143+
| A | 90-100 | brightgreen |
144+
| B | 75-89 | green |
145+
| C | 60-74 | yellow |
146+
| D | 40-59 | orange |
147+
| F | 0-39 | red |
148+
149+
## Publishing to Marketplace
150+
151+
This action is designed for GitHub Marketplace. To publish:
152+
153+
1. Create a dedicated repo: `GLINCKER/geo-audit-action`
154+
2. Copy `action.yml` and `README.md` to the repo root
155+
3. Go to the repo on GitHub, navigate to `action.yml`
156+
4. Click **"Draft a release"** → check **"Publish this Action to the GitHub Marketplace"**
157+
5. Choose category: **Code quality** (primary), **Continuous integration** (secondary)
158+
6. Tag as `v1.0.0`, create release
159+
160+
Users then reference it as:
161+
```yaml
162+
uses: GLINCKER/geo-audit-action@v1
163+
```
164+
165+
## Links
166+
167+
- [GEO Audit Rules](https://geo.glincker.com)
168+
- [npm: @glincker/geo-audit](https://www.npmjs.com/package/@glincker/geo-audit)
169+
- [GeoKit Monorepo](https://github.com/GLINCKER/geokit)
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
name: "GEO Audit — AI-Readiness Score"
2+
description: "Audit any website's AI-readiness. Scores 20 rules across discoverability, structured data, content quality, and technical readiness. Returns 0-100 score with A-F grade."
3+
author: "GLINCKER"
4+
5+
branding:
6+
icon: search
7+
color: green
8+
9+
inputs:
10+
url:
11+
description: "URL to audit (e.g. https://example.com)"
12+
required: true
13+
fail-under:
14+
description: "Minimum score threshold — fail the step if score is below this value (0 = never fail)"
15+
required: false
16+
default: "0"
17+
comment:
18+
description: "Post score as a PR comment (requires pull_request trigger and write permissions)"
19+
required: false
20+
default: "false"
21+
badge:
22+
description: "Include badge markdown snippet in the output"
23+
required: false
24+
default: "false"
25+
timeout:
26+
description: "Fetch timeout in milliseconds"
27+
required: false
28+
default: "15000"
29+
version:
30+
description: "Specific @glincker/geo-audit version to use (default: latest)"
31+
required: false
32+
default: "latest"
33+
34+
outputs:
35+
score:
36+
description: "Numeric score (0-100)"
37+
value: ${{ steps.audit.outputs.score }}
38+
grade:
39+
description: "Letter grade (A, B, C, D, or F)"
40+
value: ${{ steps.audit.outputs.grade }}
41+
badge:
42+
description: "Static shields.io badge URL for this score"
43+
value: ${{ steps.audit.outputs.badge }}
44+
result:
45+
description: "Full JSON audit result"
46+
value: ${{ steps.audit.outputs.result }}
47+
48+
runs:
49+
using: composite
50+
steps:
51+
- name: Setup Node.js
52+
uses: actions/setup-node@v4
53+
with:
54+
node-version: "20"
55+
56+
- name: Run GEO Audit
57+
id: audit
58+
shell: bash
59+
env:
60+
INPUT_URL: ${{ inputs.url }}
61+
INPUT_TIMEOUT: ${{ inputs.timeout }}
62+
INPUT_VERSION: ${{ inputs.version }}
63+
INPUT_FAIL_UNDER: ${{ inputs.fail-under }}
64+
INPUT_BADGE: ${{ inputs.badge }}
65+
run: |
66+
set -euo pipefail
67+
68+
# Install and run audit
69+
RESULT=$(npx --yes "@glincker/geo-audit@${INPUT_VERSION}" "${INPUT_URL}" --json --timeout "${INPUT_TIMEOUT}" 2>/dev/null) || {
70+
echo "::error::Failed to audit ${INPUT_URL}. Check the URL is accessible."
71+
exit 1
72+
}
73+
74+
# Parse score and grade safely
75+
SCORE=$(echo "$RESULT" | node -e "
76+
const d = require('fs').readFileSync('/dev/stdin','utf8');
77+
try { console.log(JSON.parse(d).score); }
78+
catch { console.log('-1'); }
79+
")
80+
GRADE=$(echo "$RESULT" | node -e "
81+
const d = require('fs').readFileSync('/dev/stdin','utf8');
82+
try { console.log(JSON.parse(d).grade); }
83+
catch { console.log('?'); }
84+
")
85+
86+
if [ "$SCORE" = "-1" ] || [ "$GRADE" = "?" ]; then
87+
echo "::error::Failed to parse audit result for ${INPUT_URL}"
88+
exit 1
89+
fi
90+
91+
# Grade → color mapping
92+
case "$GRADE" in
93+
A) COLOR="brightgreen"; EMOJI="🟢" ;;
94+
B) COLOR="green"; EMOJI="🟢" ;;
95+
C) COLOR="yellow"; EMOJI="🟡" ;;
96+
D) COLOR="orange"; EMOJI="🟠" ;;
97+
*) COLOR="red"; EMOJI="🔴" ;;
98+
esac
99+
100+
# Build static badge URL
101+
BADGE_URL="https://img.shields.io/badge/AI--Ready-${SCORE}%20(${GRADE})-${COLOR}"
102+
103+
# Set outputs
104+
echo "score=${SCORE}" >> "$GITHUB_OUTPUT"
105+
echo "grade=${GRADE}" >> "$GITHUB_OUTPUT"
106+
echo "badge=${BADGE_URL}" >> "$GITHUB_OUTPUT"
107+
108+
# Multiline JSON output
109+
EOF_MARKER="EOF_$(head -c 15 /dev/urandom | base64 | tr -d '/+=')"
110+
{
111+
echo "result<<${EOF_MARKER}"
112+
echo "$RESULT"
113+
echo "${EOF_MARKER}"
114+
} >> "$GITHUB_OUTPUT"
115+
116+
# Job Summary (always written — shows in Actions tab)
117+
{
118+
echo "## ${EMOJI} AI-Readiness Report"
119+
echo ""
120+
echo "| Metric | Value |"
121+
echo "|--------|-------|"
122+
echo "| **URL** | \`${INPUT_URL}\` |"
123+
echo "| **Score** | **${SCORE}/100** |"
124+
echo "| **Grade** | **${GRADE}** |"
125+
echo ""
126+
echo "![AI-Ready: ${SCORE} (${GRADE})](${BADGE_URL})"
127+
echo ""
128+
129+
# Top recommendations from JSON
130+
echo "$RESULT" | node -e "
131+
const d = require('fs').readFileSync('/dev/stdin','utf8');
132+
const j = JSON.parse(d);
133+
const recs = j.recommendations || [];
134+
if (recs.length > 0) {
135+
console.log('### Top Recommendations');
136+
console.log('');
137+
recs.slice(0, 5).forEach((r, i) => {
138+
console.log(\`\${i+1}. \${r.message} *(+\${r.impact} pts)*\`);
139+
});
140+
console.log('');
141+
}
142+
"
143+
144+
if [ "${INPUT_BADGE}" = "true" ]; then
145+
echo "### Badge Snippet"
146+
echo ""
147+
echo "\`\`\`markdown"
148+
echo "[![AI-Ready: ${SCORE} (${GRADE})](${BADGE_URL})](https://geo.glincker.com)"
149+
echo "\`\`\`"
150+
echo ""
151+
fi
152+
153+
echo "---"
154+
echo "*Powered by [GEO Audit](https://geo.glincker.com) — AI-Readiness scoring for the modern web*"
155+
} >> "$GITHUB_STEP_SUMMARY"
156+
157+
# Console output
158+
echo "${EMOJI} GEO Audit: ${INPUT_URL}"
159+
echo " Score: ${SCORE}/100 (${GRADE})"
160+
161+
# Threshold check
162+
if [ "${INPUT_FAIL_UNDER}" -gt 0 ] 2>/dev/null && [ "${SCORE}" -lt "${INPUT_FAIL_UNDER}" ]; then
163+
echo "::error::Score ${SCORE} is below threshold ${INPUT_FAIL_UNDER}"
164+
exit 1
165+
fi
166+
167+
- name: Comment on PR
168+
if: inputs.comment == 'true' && github.event_name == 'pull_request'
169+
uses: actions/github-script@v7
170+
with:
171+
script: |
172+
const score = '${{ steps.audit.outputs.score }}';
173+
const grade = '${{ steps.audit.outputs.grade }}';
174+
const badge = '${{ steps.audit.outputs.badge }}';
175+
const emoji = { A: '🟢', B: '🟢', C: '🟡', D: '🟠', F: '🔴' };
176+
const e = emoji[grade] || '⚪';
177+
178+
// Find and update existing comment (avoid spam)
179+
const { data: comments } = await github.rest.issues.listComments({
180+
owner: context.repo.owner,
181+
repo: context.repo.repo,
182+
issue_number: context.issue.number,
183+
});
184+
const existing = comments.find(c =>
185+
c.user.type === 'Bot' && c.body.includes('AI-Readiness Report')
186+
);
187+
188+
const body = [
189+
`## ${e} AI-Readiness Report`,
190+
'',
191+
`| Metric | Value |`,
192+
`|--------|-------|`,
193+
`| **URL** | \`${{ inputs.url }}\` |`,
194+
`| **Score** | **${score}/100** |`,
195+
`| **Grade** | **${grade}** |`,
196+
'',
197+
`![AI-Ready](${badge})`,
198+
'',
199+
`---`,
200+
`*Powered by [GEO Audit](https://geo.glincker.com)*`,
201+
].join('\n');
202+
203+
if (existing) {
204+
await github.rest.issues.updateComment({
205+
owner: context.repo.owner,
206+
repo: context.repo.repo,
207+
comment_id: existing.id,
208+
body,
209+
});
210+
} else {
211+
await github.rest.issues.createComment({
212+
owner: context.repo.owner,
213+
repo: context.repo.repo,
214+
issue_number: context.issue.number,
215+
body,
216+
});
217+
}

0 commit comments

Comments
 (0)