Repository navigation
146 lines (124 loc) · 6.36 KB
/
Copy pathseo.yml
File metadata and controls
146 lines (124 loc) · 6.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
name: SEO
# Technical-SEO merge gate for the static site under landing/.
#
# There is no static site generator here — 80 hand-written HTML files are
# deployed verbatim by Cloudflare Pages — so nothing except this job enforces
# canonicals, the Open Graph set, structured data, sitemap coverage or the
# extensionless URL shape that Pages actually serves. Every check is Python 3
# stdlib; no npm install, no lockfile, no supply-chain surface.
#
# The invariants seo_lint.py enforces come from the sitemaps.org protocol and
# Google Search Central's crawling-and-indexing guidance; each rule number is
# defined and explained in seo_lint.py itself. The rules that need the live site
# (every sitemap <loc> returns 200) belong in a post-deploy job, not this merge
# gate, and are deliberately not run here: they live in
# scripts/seo/verify_live.py, which is run by hand after a deploy. On a pull
# request every URL it asks for would 404, so wiring it into this workflow
# would make the gate fail on correct work.
on:
push:
branches: [main]
paths:
- 'landing/**'
- 'content/**'
- 'scripts/seo/**'
- '.github/workflows/seo.yml'
pull_request:
paths:
- 'landing/**'
- 'content/**'
- 'scripts/seo/**'
- '.github/workflows/seo.yml'
permissions:
contents: read
concurrency:
group: seo-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
seo:
name: Technical SEO gate
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
# <lastmod> comes from `git log --date=format-local` with TZ=UTC
# (`urlmap.DateSource`); a shallow clone would make every file look
# added at the boundary and silently degrade every date to mtime.
90C8
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# #974: `updated:` is required frontmatter. Before it was, an article
# could omit the field and silently date itself from the content file's
# git committer date, which a squash merge rewrites — main then went red
# on every check below the moment such a PR merged.
- name: Article frontmatter schema unit tests
run: python3 scripts/seo/test_article_schema.py
# #1476: shallow clones must not date pages from the boundary commit.
- name: Date-source unit tests (full, shallow, untracked, no repo)
run: python3 scripts/seo/test_urlmap.py
# HTTP is mocked; the post-deploy probe itself is never run in CI.
- name: Offline canonical-verifier unit tests
run: python3 scripts/seo/test_verify_live.py
# Fixture HTTP server only: production verification runs after deploy.
- name: Agent-discovery delivery regressions
run: python3 scripts/seo/test_verify_agent_discovery.py
# The audit's <lastmod> must be the generator's date, or a freshly built
# sitemap reports itself stale; this also exercises the in-memory build
# used when landing/sitemap.xml (a deploy artifact) is absent.
- name: Audit date unit tests
run: python3 scripts/seo/test_audit_dates.py
# The field report has its own section, ahead of Optional, and the
# homepage prompt asks before sending.
- name: Field-report section and permission line
run: python3 scripts/seo/test_llms_field_report.py
- name: Public skill discovery metadata and digest
run: |
python3 scripts/seo/test_build_skills.py
python3 scripts/seo/build_skills.py --check
- name: generated articles are current
run: python3 scripts/seo/build_articles.py --check
# sitemap.xml is a deploy artifact: `Deploy site (Cloudflare Pages)` runs
# `gen_sitemap.py --write` on a full checkout before uploading landing/,
# and no copy is committed. The tools that inspect it (`seo_lint.py`,
# `audit.py`, `verify_live.py`) build it from the tree when the file is
# absent, so this step is the gate on the derivation itself.
- name: sitemap derivation is valid
run: python3 scripts/seo/gen_sitemap.py --check
- name: every <head> matches the generator
run: python3 scripts/seo/fix_heads.py --check
- name: internal links and site-map footer are current
run: python3 scripts/seo/fix_links.py --check
- name: Open Graph card is the committed render
run: python3 scripts/seo/mk_og_card.py --check
- name: SEO invariants
run: python3 scripts/seo/seo_lint.py
# This gates CONTENT, not markup. factcheck.py blocks claims the
# binary cannot back (S3 backup, RBAC, HA, unqualified "semantic
# search", retracted benchmark magnitudes) and enforces number
# provenance. It was built for this change and was initially left out
# of CI — during consolidation it caught a real ERROR in a shipped
# article that had survived every other gate. Never remove it.
- name: Article claims pass the fact-check rules
run: |
python3 scripts/seo/factcheck.py --self-test
# Proves the gate can still catch what it claims to: 21 committed
# fixtures, half of which must trip a rule and half must stay clean.
# The fixtures were untracked for most of this branch's life (the
# blanket *.md ignore swallowed them) and this step reported "no
# fixtures" while exiting 2, so run it in CI, not just locally.
python3 scripts/seo/factcheck.py --fixture-check
python3 scripts/seo/factcheck.py --fail-on error
# ste_check.py is NOT a gate, deliberately. It enforces ASD-STE100, whose
# grammar rules fight the plain-language phrasing this content depends on:
# the articles are targeted at the exact strings a person types, and STE
# rewrites them into controlled-language prose that no longer matches. The
# script stays in the repo as a local authoring aid — run it by hand with
# python3 scripts/seo/ste_check.py content/answers/*.md content/compare/*.md
# and treat its output as advice, not as a merge condition. Do not add it
# back to this workflow; the false-claim gate above is factcheck.py, and
# that one does block.
- name: Full audit (report only, never fails the build)
if: always()
run: python3 scripts/seo/audit.py --root landing --fail-on critical --max-findings 99