-
Notifications
You must be signed in to change notification settings - Fork 10
281 lines (261 loc) · 14.2 KB
/
Copy pathbuild.yml
File metadata and controls
281 lines (261 loc) · 14.2 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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# Build and verify the ontology corpus — NO DEPLOY, BY DESIGN.
#
# This repository publishes a corpus and the pipeline that compiles it. This
# workflow builds and gates that corpus; it deploys nothing. The deploy path is
# deliberately absent: no GitHub Pages step, no publishing action, and no
# reference to any secret anywhere in this file. `permissions: contents: read`
# is the whole permission set — declared at workflow and job level both — so
# this workflow cannot write to the repository even if a step tried.
#
# The repository root does carry a CNAME (narrativegoldmine.com) for the
# published site, but no step below reads, writes or acts on it; the Pages push
# lives in the private publishing CI (docs/ci-cd/build-and-gates.md §1).
#
# That private CI runs this same seven-stage pipeline and then the steps that
# cannot be reproduced from a clone: SPA/WASM build, browser smoke test and the
# gh-pages deploy. Its nine gates are listed in docs/ci-cd/build-and-gates.md §2;
# three of them re-appear here (gate 2 pipeline unit tests, gate 4 build
# validation, gate 5 standalone validation), and the other six cover the explorer
# and the built site. The secret scan and the class-count contract below are
# specific to this extracted corpus and have no counterpart in that list. What
# you see here is the half that can be reproduced by anyone with a clone, no
# credentials required.
#
# The corpus itself is mostly AI-generated synthetic content produced under human
# direction, by design. It is an ontology testbed, not an authoritative
# encyclopaedia; its provenance (did:nostr, generatedAtTime, URNs) attests
# traceable generation under human direction, not human authorship. The pipeline
# emits that framing rather than leaving it to prose — see `corpus.nature:
# "synthetic"` in dist-ci/data/graph/stats.json.
#
# What this workflow proves, on every push and pull request:
# 1. the corpus contains no credential-shaped strings (load-bearing: the
# public corpus feeds the export — projected Markdown shares its privacy filter)
# 2. the NGG1 binary writer still matches its 183-byte golden fixture
# (pipeline/tests/test_emit_graph_tiers.py, pipeline/tests/fixtures/ngg1-3n2e.bin)
# 3. the 7-stage pipeline runs end to end from ontology/pages/
# 4. the build produced exactly 8138 OWL classes — a hard contract gate, not
# a printed statistic
# 5. pipeline.validate reports zero errors over the corpus
# 6. every input file is accounted for — parsed, rejected with a reason, or
# excluded — and the strict build refuses to publish a corpus containing a
# malformed page (pipeline/census.py)
# 7. the built class set matches the committed sorted class-IRI set, so an
# equal-count identity substitution fails even though the count agrees;
# the same gate checks the consumer schema shape and scans every public
# artefact for private identifiers (pipeline/release_gate.py)
# 8. the generation manifest re-verifies: every artefact's SHA-256 matches
# what the build recorded (pipeline/manifest.py)
#
# Reproduce locally:
# pip install "rdflib>=7.0.0" pytest
# python -m pytest pipeline/tests -q
# python -m pipeline.build ontology/pages dist-ci --strict
# python -m pipeline.release_gate dist-ci --pages ontology/pages \
# --expected-classes 8138
# python -m pipeline.manifest dist-ci
name: Build and verify
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
env:
# The corpus contract. Both dist-ci/data/graph/stats.json (`classes`) and
# dist-ci/data/ontology.json (`class[]` length) must report this many OWL
# classes or the build fails. Change it only alongside the corpus.
#
# This stays a hand-typed, independently-authored number (ADR-2003): an
# auto-derived expectation moves with any parse regression and can therefore
# never detect one. GATE 5 adds *membership* on top of this *count*, because a
# count cannot see an equal-count identity substitution — delete one class,
# add another, and this gate still passes. Both must move in the same commit
# as any corpus size change; the gate fails if they disagree.
EXPECTED_CLASSES: '8138'
jobs:
build:
name: Pipeline build and gates
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
# ------------------------------------------------------------------
# GATE 1 — secret scan over the published corpus (cheapest first)
# ------------------------------------------------------------------
# ontology/pages/*.md feeds public exports. A leaked key in a page is a leaked
# key on the internet, so this gate blocks the build rather than warning.
#
# Every pattern is anchored to a token-shaped suffix on purpose. A bare
# `sk-` substring match hits 1107 of the 7874 corpus files (risk-, task-,
# disk-, desk-assistant …), which would make the gate useless noise; the
# anchored form `sk-[A-Za-z0-9]{20,}` matches 0. Likewise the bare prefix
# `[Bb]earer ` used below appears in 64 pages as HTTP prose, while
# `[Bb]earer <20+ token chars>` matches 0. Verified against the corpus at
# 7874 pages: 0 hits total.
- name: Secret scan (corpus must contain no credential-shaped strings)
run: |
set -uo pipefail
patterns='(sk-[A-Za-z0-9]{20,}|pplx-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{20,}|gho_[A-Za-z0-9]{36}|AKIA[0-9A-Z]{16}|-----BEGIN[A-Z ]*PRIVATE KEY-----|[Bb]earer [A-Za-z0-9._~+/-]{20,})'
# Actions runs `run:` steps under `bash -e`, and `set -uo pipefail`
# does not clear it. A plain `hits=$(grep …)` therefore aborts the step
# the instant grep exits 1 — i.e. on a *clean* corpus, the pass case.
# `|| rc=$?` puts the assignment in a tested context and keeps the code.
rc=0
hits=$(grep -rInE "$patterns" ontology/) || rc=$?
if [ "$rc" -eq 0 ]; then
echo "Secret scan FAILED — credential-shaped string(s) in the published corpus:"
echo "$hits" | cut -c1-200
exit 1
fi
# grep exits 1 for "no match" and 2+ for a real error. Only 1 is a pass;
# a broken scan must never be mistaken for a clean corpus.
if [ "$rc" -ne 1 ]; then
echo "Secret scan ERRORED: grep exited $rc"
exit 1
fi
echo "Secret scan passed: no credential-shaped strings under ontology/"
- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: '3.12'
- name: Install Python dependencies
run: pip install "rdflib>=7.0.0" pytest
# ------------------------------------------------------------------
# GATE 2 — pipeline unit tests
# ------------------------------------------------------------------
# Includes the byte-exact 183-byte NGG1 golden (explorer/FORMAT-NGG1.md §7), parsed
# by a struct reader written independently of the writer, plus the
# overview.json consumer contract (40 nodes: 6 domains then 34
# categories, order frozen) and layout determinism.
- name: Pipeline unit tests (pytest)
run: python -m pytest pipeline/tests -q
# ------------------------------------------------------------------
# BUILD — the 7 stages: parse, validate, Turtle, WebVOWL JSON,
# page API, search index, NGG1 graph tiers
# ------------------------------------------------------------------
# Both strict and non-strict canonical builds now refuse malformed or
# ambiguously public input and validation errors before promotion.
# Strict remains explicit for release-contract and explorer checks.
# Local authoring diagnostics use census/validate directly; public output
# receives aggregate diagnostics and projected Markdown only.
# Successful staging promotion replaces obsolete generated pages.
- name: Run the JSON-LD pipeline (strict)
run: |
set -euo pipefail
python -m pipeline.build ontology/pages dist-ci --strict
echo "--- outputs ---"
ls -la dist-ci/data/
ls -la dist-ci/data/graph/
echo "--- input census ---"
python - <<'PY'
import json
from pathlib import Path
c = json.loads(Path("dist-ci/api/census.json").read_text())
print(f" input files : {c['input_files']}")
print(f" parsed : {c['parsed']} ({c['public_pages']} public)")
print(f" rejected : {c['rejected']}")
print(f" excluded : {c['excluded']}")
print(f" balanced : {c['balanced']}")
PY
# ------------------------------------------------------------------
# GATE 3 — corpus contract: the build must produce 8138 OWL classes
# ------------------------------------------------------------------
# Read from the artefact the pipeline actually wrote, not from stdout.
# A silent parse regression (a dropped JSON-LD fence, a changed public
# filter) shows up here as a number, not as a warning nobody reads.
- name: Assert corpus contract (8138 classes)
run: |
set -euo pipefail
python - <<'PY'
import json, os, sys
from pathlib import Path
expected = int(os.environ["EXPECTED_CLASSES"])
stats = json.loads(Path("dist-ci/data/graph/stats.json").read_text())
vowl = json.loads(Path("dist-ci/data/ontology.json").read_text())
checks = {
"stats.json classes": stats["classes"],
"ontology.json class[] length": len(vowl["class"]),
}
failed = [f"{k}: expected {expected}, got {v}" for k, v in checks.items() if v != expected]
for k, v in checks.items():
print(f" {k}: {v}")
print(f" stats.json nodes: {stats['nodes']}, pages: {stats['pages']}, "
f"domains: {stats['domains']}, categories: {stats['categories']}")
print(f" edges declared: {stats['edges']['declared']}, "
f"resolvable: {stats['edges']['resolvable']}")
if failed:
print("Corpus contract FAILED:")
for line in failed:
print(f" {line}")
sys.exit(1)
print(f"Corpus contract OK: {expected} OWL classes")
PY
# ------------------------------------------------------------------
# GATE 4 — validation must report zero errors
# ------------------------------------------------------------------
# pipeline.validate exits 1 on any error. Warnings do not fail the build,
# and the corpus currently carries none: the report reads 0 errors, 0
# warnings, 1401 info, all of the info entries MULTI_PARENT. That code was
# a warning until it was reclassified — multiple inheritance is deliberate
# here, so 957 of the old 961 warnings were the design, not a defect list
# (docs/ci-cd/build-and-gates.md §3). The bridging it counts is published
# in stats.json (`bridging`), bridges.json and overview.json.
- name: Validate corpus (0 errors required)
run: python -m pipeline.validate ontology/pages
# ------------------------------------------------------------------
# GATE 5 — identity, schema and publication-visibility contracts
# ------------------------------------------------------------------
# The count above is one number, and three failures leave it unchanged:
# an equal-count identity substitution, an altered publication visibility,
# and a shape drift that still parses as JSON but no longer parses in the
# explorer. This gate compares the built class set against the committed
# sorted class-IRI set (pipeline/contracts/class-identity.txt), asserts
# the consumer-shaped class/property arrays are present, and re-derives
# the private identifiers from source to prove none of them occurs in a
# published artefact. Private identifiers are never written into dist-ci.
- name: Assert release contracts (identity set, schema, visibility)
run: |
set -euo pipefail
python -m pipeline.release_gate dist-ci \
--pages ontology/pages \
--expected-classes "$EXPECTED_CLASSES"
# ------------------------------------------------------------------
# GATE 6 — the generation manifest re-verifies
# ------------------------------------------------------------------
# Every artefact's SHA-256, the source revision, the counts and a
# generation id are recorded with the export. Re-hashing the tree here
# proves the manifest describes the artefacts actually uploaded.
- name: Verify generation manifest
run: python -m pipeline.manifest dist-ci
# ------------------------------------------------------------------
# ARTEFACT — build output, downloadable from the run page. Nothing is
# published anywhere; the artefact expires with the run's retention.
# ------------------------------------------------------------------
# `if: always()` means this also runs when gate 1 or 2 failed, in which
# case dist-ci was never created. `if-no-files-found: warn` keeps that case
# to one honest failure — the gate that actually broke — instead of adding
# a second, misleading upload failure on top of it.
# Archive first: title mirrors intentionally preserve ':' and case-sensitive
# names, which upload-artifact cannot transport as loose files.
# Download dist-ci then run: tar -xzf dist-ci.tar.gz
- name: Package build output without filename normalisation
if: always()
run: |
set -euo pipefail
if [ -d dist-ci ]; then
tar -czf dist-ci.tar.gz dist-ci
fi
- name: Upload build output
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: dist-ci
path: dist-ci.tar.gz
retention-days: 14
if-no-files-found: warn