Skip to content

Commit 42564db

Browse files
authored
fix(docs): make SEO sync workflow compatible with protected main (#151)
* fix(docs): avoid direct main pushes in SEO sync workflow * fix(docs): validate MIN_SITEMAP_URLS strictly when provided * chore(docs): log full stack for SEO check failures
1 parent 791fdaf commit 42564db

File tree

2 files changed

+76
-8
lines changed

2 files changed

+76
-8
lines changed

.github/workflows/docs-seo-aeo.yml

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ on:
1515
- 'docs/pnpm-lock.yaml'
1616
workflow_dispatch:
1717

18+
env:
19+
SITE_URL: https://ralphstarter.ai
20+
1821
permissions:
1922
contents: write
23+
pull-requests: write
2024

2125
jobs:
2226
sync-seo-assets:
@@ -48,28 +52,47 @@ jobs:
4852
- name: Validate SEO/AEO outputs
4953
working-directory: docs
5054
run: pnpm seo:check
55+
env:
56+
SITE_URL: ${{ env.SITE_URL }}
5157

5258
- name: Sync generated assets into docs/static
5359
working-directory: docs
5460
run: pnpm seo:sync
5561

56-
- name: Commit synced assets
62+
- name: Commit synced assets and open PR
63+
env:
64+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5765
run: |
5866
if git diff --quiet -- docs/static/sitemap.xml docs/static/llms.txt docs/static/llms-full.txt docs/static/docs.json docs/static/docs-urls.txt docs/static/ai-index.json docs/static/sidebar.json; then
5967
echo "No SEO/AEO asset changes to commit."
6068
exit 0
6169
fi
6270
71+
BRANCH_NAME="automation/docs-seo-aeo-sync"
72+
6373
git config user.name "github-actions[bot]"
6474
git config user.email "github-actions[bot]@users.noreply.github.com"
75+
git checkout -B "${BRANCH_NAME}"
6576
git add docs/static/sitemap.xml docs/static/llms.txt docs/static/llms-full.txt docs/static/docs.json docs/static/docs-urls.txt docs/static/ai-index.json docs/static/sidebar.json
6677
git commit -m "chore(docs): sync generated SEO/AEO assets [skip ci]"
67-
git push
78+
git push origin "${BRANCH_NAME}" --force-with-lease
79+
80+
EXISTING_PR=$(gh pr list --head "${BRANCH_NAME}" --base main --state open --json number --jq '.[0].number // empty')
81+
if [ -n "${EXISTING_PR}" ]; then
82+
echo "Updated existing PR #${EXISTING_PR}"
83+
exit 0
84+
fi
85+
86+
gh pr create \
87+
--base main \
88+
--head "${BRANCH_NAME}" \
89+
--title "chore(docs): sync generated SEO/AEO assets" \
90+
--body "Automated sync of generated docs SEO/AEO artifacts from docs build."
6891
6992
- name: Notify search engines
7093
run: |
7194
set +e
72-
SITEMAP_URL="https://ralphstarter.ai/sitemap.xml"
95+
SITEMAP_URL="${SITE_URL}/sitemap.xml"
7396
7497
echo "Pinging Bing sitemap endpoint..."
7598
curl --connect-timeout 5 --max-time 15 -fsS "https://www.bing.com/ping?sitemap=${SITEMAP_URL}" >/dev/null && echo "Bing ping sent." || echo "Bing ping failed (non-fatal)."
@@ -79,7 +102,7 @@ jobs:
79102
80103
if [ -n "${INDEXNOW_KEY}" ]; then
81104
echo "Submitting IndexNow URL update..."
82-
curl --connect-timeout 5 --max-time 15 -fsS "https://api.indexnow.org/indexnow?url=https://ralphstarter.ai/docs/intro&key=${INDEXNOW_KEY}" >/dev/null && echo "IndexNow submitted." || echo "IndexNow submission failed (non-fatal)."
105+
curl --connect-timeout 5 --max-time 15 -fsS "https://api.indexnow.org/indexnow?url=${SITE_URL}/docs/intro&key=${INDEXNOW_KEY}" >/dev/null && echo "IndexNow submitted." || echo "IndexNow submission failed (non-fatal)."
83106
else
84107
echo "INDEXNOW_KEY not configured; skipping IndexNow submission."
85108
fi

docs/scripts/check-seo-aeo.cjs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,31 @@ const buildDir = path.join(rootDir, 'build');
66
const staticDir = path.join(rootDir, 'static');
77
const siteUrl = process.env.SITE_URL || 'https://ralphstarter.ai';
88
const expectedOrigin = new URL(siteUrl).origin;
9+
// Keep a minimum URL floor so we catch truncated/partial sitemap generation.
10+
const defaultMinSitemapUrls = 10;
11+
12+
function resolveMinSitemapUrls() {
13+
const rawValue = process.env.MIN_SITEMAP_URLS;
14+
if (rawValue === undefined || rawValue.trim() === '') {
15+
return defaultMinSitemapUrls;
16+
}
17+
18+
const normalized = rawValue.trim();
19+
if (!/^\d+$/.test(normalized)) {
20+
throw new Error(
21+
`Invalid MIN_SITEMAP_URLS value "${rawValue}". Expected a positive integer.`
22+
);
23+
}
24+
25+
const parsedValue = Number(normalized);
26+
if (!Number.isSafeInteger(parsedValue) || parsedValue <= 0) {
27+
throw new Error(
28+
`Invalid MIN_SITEMAP_URLS value "${rawValue}". Expected a positive integer.`
29+
);
30+
}
31+
32+
return parsedValue;
33+
}
934

1035
const requiredBuildFiles = [
1136
'sitemap.xml',
@@ -24,6 +49,8 @@ function assertFileExists(fullPath, label) {
2449
}
2550

2651
function main() {
52+
const minSitemapUrls = resolveMinSitemapUrls();
53+
2754
assertFileExists(buildDir, 'Build directory');
2855

2956
for (const file of requiredBuildFiles) {
@@ -34,8 +61,10 @@ function main() {
3461
const sitemap = fs.readFileSync(sitemapPath, 'utf8');
3562
const locMatches = [...sitemap.matchAll(/<loc>([^<]+)<\/loc>/g)].map((match) => match[1]);
3663

37-
if (locMatches.length < 10) {
38-
throw new Error(`Sitemap has too few URLs (${locMatches.length}).`);
64+
if (locMatches.length < minSitemapUrls) {
65+
throw new Error(
66+
`Sitemap has too few URLs (${locMatches.length}). Expected at least ${minSitemapUrls}.`
67+
);
3968
}
4069

4170
if (!locMatches.some((url) => url.includes('/docs/intro'))) {
@@ -60,7 +89,13 @@ function main() {
6089
}
6190

6291
const docsManifestRaw = fs.readFileSync(path.join(buildDir, 'docs.json'), 'utf8');
63-
const docsManifest = JSON.parse(docsManifestRaw);
92+
let docsManifest;
93+
try {
94+
docsManifest = JSON.parse(docsManifestRaw);
95+
} catch (error) {
96+
const reason = error instanceof Error ? error.message : String(error);
97+
throw new Error(`Malformed docs.json manifest: ${reason}. Please regenerate docs artifacts.`);
98+
}
6499
if (!Array.isArray(docsManifest.docs) || docsManifest.docs.length === 0) {
65100
throw new Error('docs.json manifest has no docs entries.');
66101
}
@@ -77,4 +112,14 @@ function main() {
77112
console.log(`- docs manifest entries: ${docsManifest.docs.length}`);
78113
}
79114

80-
main();
115+
try {
116+
main();
117+
process.exitCode = 0;
118+
} catch (error) {
119+
if (error instanceof Error) {
120+
console.error('Validation failed:\n', error.stack || error.message);
121+
} else {
122+
console.error('Validation failed:', error);
123+
}
124+
process.exit(1);
125+
}

0 commit comments

Comments
 (0)