Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions .github/workflows/audit-articles.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
name: Article Audit

# Reusable workflow — weekly audit of all published blog articles.
# Checks structural compliance, broken internal links, file sync, and style violations.
# Opens (or updates) a GitHub Issue with labeled findings if problems are found.
#
# Client usage:
# jobs:
# audit:
# uses: cybrixcc/leadhunter-engine/.github/workflows/audit-articles.yml@master
# with:
# config_path: ./config.yml
# blog_glob: src/app/blog
# secrets: inherit

on:
workflow_call:
inputs:
config_path:
description: 'Path to config.yml in the calling repo'
required: false
default: './config.yml'
type: string
blog_glob:
description: 'Path to blog directory containing article subdirectories'
required: false
default: 'src/app/blog'
type: string
secrets:
TELEGRAM_BOT_TOKEN:
required: false
TELEGRAM_CHAT_ID:
required: false

workflow_dispatch:
inputs:
blog_glob:
description: 'Path to blog directory'
required: false
default: 'src/app/blog'
type: string

permissions:
contents: read
issues: write

jobs:
audit:
runs-on: ubuntu-latest

steps:
- name: Checkout calling repository
uses: actions/checkout@v4

- name: Checkout engine
uses: actions/checkout@v4
with:
repository: cybrixcc/leadhunter-engine
path: .engine
ref: master
token: ${{ secrets.GH_PAT }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Load config and set env vars
run: |
CONFIG="${{ inputs.config_path || './config.yml' }}"
if [ -f "$CONFIG" ]; then
SITE_NAME=$(grep '^site_name:' "$CONFIG" | sed 's/site_name:\s*//' | tr -d '"' | tr -d "'")
echo "SITE_NAME=${SITE_NAME}" >> $GITHUB_ENV
fi

- name: Copy engine scripts
run: cp -r .engine/scripts ./scripts

- name: Run article audit
id: audit
run: |
node scripts/audit-articles.mjs \
--blog-glob="${{ inputs.blog_glob || 'src/app/blog' }}"
echo "exit_code=0" >> $GITHUB_OUTPUT
continue-on-error: true

- name: Capture exit code
if: always()
run: echo "AUDIT_EXIT=$?" >> $GITHUB_ENV

- name: Open or update GitHub Issue if issues found
if: always() && (steps.audit.outcome == 'failure' || steps.audit.outputs.exit_code != '0')
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');

let body = '## Weekly Article Audit\n\nAudit script failed to produce a report. Check the Actions log.';
if (fs.existsSync('audit-report.md')) {
body = fs.readFileSync('audit-report.md', 'utf8');
}

const label = 'audit';

// Ensure label exists
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label,
color: 'e4e669',
description: 'Automated article audit findings',
});
} catch (_) {
// Label already exists — ignore
}

// Check for existing open audit issue
const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: label,
state: 'open',
});

const today = new Date().toISOString().slice(0, 10);

if (existing.data.length > 0) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existing.data[0].number,
body: `### Re-audit — ${today}\n\n${body}`,
});
console.log(`Commented on existing issue #${existing.data[0].number}`);
} else {
const created = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Article Audit: issues found — ${today}`,
body,
labels: [label],
});
console.log(`Created issue #${created.data.number}`);
}

- name: Close resolved audit issue if all clear
if: steps.audit.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'audit',
state: 'open',
});
for (const issue of existing.data) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Audit re-run passed — all issues resolved. Closing.`,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
});
console.log(`Closed resolved issue #${issue.number}`);
}

- name: Send Telegram notification on errors
if: steps.audit.outcome == 'failure' && secrets.TELEGRAM_BOT_TOKEN != ''
run: |
LABEL="${SITE_NAME:-Blog}"
ISSUES_URL="https://github.com/${{ github.repository }}/issues?q=label%3Aaudit+is%3Aopen"
MESSAGE="${LABEL} audit found issues. Review: ${ISSUES_URL}"
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
-d text="$MESSAGE" \
> /dev/null
163 changes: 163 additions & 0 deletions .github/workflows/generate-briefs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
name: Generate Briefs

# Reusable workflow — keeps the content queue full by auto-generating briefs
# when the number of "ready" topics drops below the minimum threshold.
#
# Designed to run daily at 08:00 UTC — one hour before generate-article.yml.
#
# Client usage:
# jobs:
# generate-briefs:
# uses: cybrixcc/leadhunter-engine/.github/workflows/generate-briefs.yml@master
# with:
# config_path: ./config.yml
# secrets: inherit

on:
workflow_call:
inputs:
config_path:
description: 'Path to config.yml in the calling repo'
required: false
default: './config.yml'
type: string
min_ready:
description: 'Minimum number of ready topics to maintain (default: 5)'
required: false
default: 5
type: number
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workflow min_ready input is silently ignored

Medium Severity

The workflow declares a min_ready input (default 5) but never passes it to the script. The script hardcodes MIN_READY = 5 and doesn't accept a --min-ready CLI argument. Any caller setting min_ready: 10 (or any other value) in workflow inputs will have it silently ignored, always using the hardcoded threshold of 5.

Additional Locations (1)

Fix in Cursor Fix in Web

force_count:
description: 'Force generate this many briefs regardless of queue size (0 = auto)'
required: false
default: 0
type: number
dry_run:
description: 'Dry run — preview without writing files'
required: false
default: false
type: boolean
secrets:
ANTHROPIC_API_KEY:
required: true
GH_PAT:
required: false
TELEGRAM_BOT_TOKEN:
required: false
TELEGRAM_CHAT_ID:
required: false

workflow_dispatch:
inputs:
force_count:
description: 'Force generate this many briefs (0 = auto)'
required: false
default: 0
type: number
dry_run:
description: 'Dry run — preview without writing files'
required: false
default: false
type: boolean

permissions:
contents: write

jobs:
generate-briefs:
runs-on: ubuntu-latest

steps:
- name: Checkout calling repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}

- name: Checkout engine
uses: actions/checkout@v4
with:
repository: cybrixcc/leadhunter-engine
path: .engine
ref: master
token: ${{ secrets.GH_PAT }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: .engine/package.json

- name: Install engine dependencies
run: npm ci --prefix .engine

- name: Load config and set env vars
run: |
CONFIG="${{ inputs.config_path || './config.yml' }}"
if [ -f "$CONFIG" ]; then
SITE_NAME=$(grep '^site_name:' "$CONFIG" | sed 's/site_name:\s*//' | tr -d '"' | tr -d "'")
GIT_USER_NAME=$(grep '^git_user_name:' "$CONFIG" | sed 's/git_user_name:\s*//' | tr -d '"' | tr -d "'" || echo "Blog Bot")
GIT_USER_EMAIL=$(grep '^git_user_email:' "$CONFIG" | sed 's/git_user_email:\s*//' | tr -d '"' | tr -d "'" || echo "bot@example.com")
echo "SITE_NAME=${SITE_NAME}" >> $GITHUB_ENV
echo "GIT_USER_NAME=${GIT_USER_NAME}" >> $GITHUB_ENV
echo "GIT_USER_EMAIL=${GIT_USER_EMAIL}" >> $GITHUB_ENV
fi

- name: Setup Git
run: |
git config user.name "${GIT_USER_NAME:-Blog Bot}"
git config user.email "${GIT_USER_EMAIL:-bot@example.com}"

- name: Copy engine scripts
run: cp -r .engine/scripts ./scripts

- name: Generate briefs
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
ARGS=""
if [ "${{ inputs.dry_run }}" = "true" ]; then ARGS="--dry-run"; fi
if [ "${{ inputs.force_count }}" != "0" ] && [ -n "${{ inputs.force_count }}" ]; then
ARGS="$ARGS --count=${{ inputs.force_count }}"
fi
MIN_READY="${{ inputs.min_ready }}"
if [ -n "$MIN_READY" ] && [ "$MIN_READY" != "5" ]; then
ARGS="$ARGS --min-ready=$MIN_READY"
fi
node scripts/generate-briefs.mjs $ARGS

- name: Commit and push if changed
if: inputs.dry_run != true
run: |
git add docs/briefs/ CONTENT_PLAN.md
if git diff --staged --quiet; then
echo "No new briefs generated — queue already full."
else
ADDED=$(git diff --staged --name-only | grep 'docs/briefs/' | wc -l | tr -d ' ')
git commit -m "chore: auto-generate ${ADDED} brief(s) to refill content queue"
git pull --rebase origin $(git branch --show-current)
git push origin $(git branch --show-current)
echo "BRIEFS_ADDED=${ADDED}" >> $GITHUB_ENV
fi

- name: Send Telegram notification
if: env.BRIEFS_ADDED != '' && secrets.TELEGRAM_BOT_TOKEN != ''
run: |
LABEL="${SITE_NAME:-Blog}"
MESSAGE="${LABEL}: ${BRIEFS_ADDED} new brief(s) added to content queue"
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
-d text="$MESSAGE" \
> /dev/null

- name: Send failure notification
if: failure()
run: |
if [ -n "${{ secrets.TELEGRAM_BOT_TOKEN }}" ] && [ -n "${{ secrets.TELEGRAM_CHAT_ID }}" ]; then
LABEL="${SITE_NAME:-Blog}"
MESSAGE="${LABEL}: Brief generation failed. Check Actions: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
-d text="$MESSAGE" \
> /dev/null
fi
26 changes: 17 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@ Shared engine for automated SEO article generation for client blogs. Cloned from
## Commands

```bash
npm install # install deps (Node 22+)
node scripts/generate-article.mjs # generate next ready topic
node scripts/generate-article.mjs --dry-run # preview without committing
node scripts/generate-article.mjs --topic=6 # generate specific topic
node scripts/gsc-index-check.mjs # check/submit GSC index
node scripts/gsc-keyword-performance.mjs # weekly keyword report
node scripts/ai-citation-research.mjs # AI citation research
bash scripts/geo-health-check.sh # GEO content health score
npm install # install deps (Node 22+)
node scripts/generate-briefs.mjs # generate briefs if queue < 5 ready
node scripts/generate-briefs.mjs --count=3 # force generate 3 briefs
node scripts/generate-briefs.mjs --dry-run # preview without writing files
node scripts/generate-article.mjs # generate next ready topic
node scripts/generate-article.mjs --dry-run # preview without committing
node scripts/generate-article.mjs --topic=6 # generate specific topic
node scripts/audit-articles.mjs # audit all published articles
node scripts/audit-articles.mjs --blog-glob=src/app/blog # custom blog directory path
node scripts/gsc-index-check.mjs # check/submit GSC index
node scripts/gsc-keyword-performance.mjs # weekly keyword report
node scripts/ai-citation-research.mjs # AI citation research
bash scripts/geo-health-check.sh # GEO content health score
```

## Architecture
Expand Down Expand Up @@ -103,10 +108,13 @@ All workflows support `workflow_call` with `config_path` input. They:

| Workflow | Trigger in client |
|---------|------------------|
| `generate-article.yml` | schedule or manual, generates articles |
| `generate-briefs.yml` | daily 08:00 UTC, refills content queue if < 5 ready topics |
| `generate-article.yml` | daily 09:00 UTC or manual, generates next article |
| `audit-articles.yml` | weekly (Sundays), audits all published articles |
| `gsc-index-check.yml` | schedule, submits unindexed pages to GSC |
| `gsc-keyword-performance.yml` | schedule, weekly keyword report → GitHub issue |
| `geo-health-check.yml` | schedule, GEO score → GitHub issue |
| `ai-article-review.yml` | PR trigger, reviews new article PRs with auto-fixes |
| `ai-article-review.yml` | PR trigger, auto-reviews blog article PRs |

### Required secrets (in client repo)
Expand Down
Loading