-
Notifications
You must be signed in to change notification settings - Fork 0
444 lines (404 loc) · 19.9 KB
/
Copy pathroster-health.yml
File metadata and controls
444 lines (404 loc) · 19.9 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
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
name: Roster Health
# Runs nightly and on changes to roster/index.yaml to verify every
# Eidolon in the roster is EIIS-conformant and reachable.
#
# Triggers (note: pull_request is intentionally omitted — same-org PR
# branches push to this repo directly and already fire the `push` trigger
# with the canonical workflow token. The pull_request token is scoped
# down for forks and rejects cross-repo `gh` calls against external
# Eidolon repos. Re-add the pull_request trigger only if/when we accept
# fork contributions and re-architect the cross-repo verifier.)
on:
push:
paths:
- 'roster/**'
- 'cli/**'
- 'schemas/**'
- '.github/workflows/roster-health.yml'
schedule:
# Nightly at 03:00 UTC — catches upstream Eidolon repos going bad
- cron: '0 3 * * *'
workflow_dispatch:
jobs:
validate-roster:
name: Validate roster schema
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq
# mikefarah/yq
sudo wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- name: Validate roster/index.yaml structure
run: |
set -euo pipefail
yq eval '.' roster/index.yaml > /tmp/roster.json
# Every entry must have required fields
for name in $(jq -r '.eidolons[].name' /tmp/roster.json); do
echo "Validating $name..."
jq -e --arg n "$name" '.eidolons[] | select(.name == $n) |
.methodology.name and .methodology.version and .methodology.cycle and
.source.repo and
.versions.latest and
.handoffs.upstream and .handoffs.downstream' \
/tmp/roster.json > /dev/null || { echo "::error::$name missing required fields"; exit 1; }
done
- name: Validate JSON schemas themselves
run: |
for schema in schemas/*.json; do
echo "Checking $schema is valid JSON..."
jq empty "$schema"
done
- name: Validate shipped-status integrity metadata posture
# Story 5.H: a roster entry with status=shipped must declare release
# integrity metadata under versions.releases.<latest>. Compatibility
# mode (integrity.enforcement: warn) makes this a warning; strict
# mode promotes it to a hard failure. in_construction Eidolons skip
# — consistent with the EIIS-conformance matrix job.
run: |
set -euo pipefail
enforcement="$(yq -r '.integrity.enforcement // "warn"' roster/index.yaml)"
missing_count=0
while IFS= read -r name; do
[[ -n "$name" ]] || continue
status="$(yq -r ".eidolons[] | select(.name == \"$name\") | .status" roster/index.yaml)"
if [[ "$status" != "shipped" ]]; then
continue
fi
latest="$(yq -r ".eidolons[] | select(.name == \"$name\") | .versions.latest" roster/index.yaml)"
meta="$(yq -o=json ".eidolons[] | select(.name == \"$name\") | .versions.releases.\"$latest\" // {}" roster/index.yaml)"
if [[ "$meta" == "{}" ]]; then
if [[ "$enforcement" == "strict" ]]; then
echo "::error::$name is shipped at v$latest but versions.releases.\"$latest\" is missing"
missing_count=$((missing_count + 1))
else
echo "::warning::$name is shipped at v$latest without release integrity metadata (compatibility mode)"
fi
fi
done < <(yq -r '.eidolons[].name' roster/index.yaml)
if [[ "$missing_count" -gt 0 ]]; then
exit 1
fi
parse-mcp-catalogue:
name: Validate MCP catalogue
runs-on: ubuntu-latest
needs: validate-roster
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq
sudo wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- name: Parse roster/mcps.yaml
run: |
set -euo pipefail
yq eval '.' roster/mcps.yaml > /dev/null
echo "roster/mcps.yaml parses cleanly"
- name: Validate MCP catalogue schema is valid JSON
run: |
jq empty schemas/mcp-catalogue.schema.json
jq empty schemas/mcp-lockfile.schema.json
echo "MCP schemas are valid JSON"
- name: Check catalogue entries have required fields
run: |
set -euo pipefail
yq -o=json eval '.' roster/mcps.yaml > /tmp/mcps.json
for name in $(jq -r '.mcps[].name' /tmp/mcps.json); do
echo "Validating MCP entry: $name ..."
jq -e --arg n "$name" '.mcps[] | select(.name == $n) |
.display_name and .scope and .kind and .description and
.source.type and .versions.latest and .versions.pins.stable and
(.health.probes | length > 0)' \
/tmp/mcps.json > /dev/null \
|| { echo "::error::MCP $name missing required fields"; exit 1; }
done
echo "All MCP catalogue entries valid"
mcp-catalogue-digest-reachable:
name: MCP catalogue digest reachability (non-fatal)
runs-on: ubuntu-latest
needs: parse-mcp-catalogue
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq curl
sudo wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- name: Probe oci-image digests reachability on ghcr.io
run: |
set -uo pipefail
yq -o=json eval '.' roster/mcps.yaml > /tmp/mcps.json
warn_count=0
while IFS= read -r entry; do
name="$(printf '%s' "$entry" | jq -r '.name')"
kind="$(printf '%s' "$entry" | jq -r '.kind')"
if [ "$kind" != "oci-image" ]; then
continue
fi
image="$(printf '%s' "$entry" | jq -r '.source.image // ""')"
digest="$(printf '%s' "$entry" \
| jq -r '.versions.releases | to_entries[0].value.digest // ""')"
if [ -z "$image" ] || [ -z "$digest" ]; then
echo "::warning::$name: no image or digest in catalogue — skipping reachability probe"
continue
fi
full_ref="${image}@${digest}"
namespace="${image#ghcr.io/}"
repo_part="${namespace#*/}"
ns_part="${namespace%%/*}"
token_url="https://ghcr.io/token?scope=repository:${ns_part}/${repo_part}:pull"
token_json="$(curl -fsSL "$token_url" 2>/dev/null)" || {
echo "::warning::$name: could not fetch ghcr.io token (offline?)"
warn_count=$((warn_count + 1))
continue
}
token="$(printf '%s' "$token_json" | jq -r '.token // empty')"
if [ -z "$token" ]; then
echo "::warning::$name: empty token from ghcr.io"
warn_count=$((warn_count + 1))
continue
fi
manifest_url="https://ghcr.io/v2/${ns_part}/${repo_part}/manifests/${digest}"
http_status="$(curl -fsI \
-H "Authorization: Bearer ${token}" \
-o /dev/null \
-w '%{http_code}' \
"$manifest_url" 2>/dev/null)" || http_status=""
if [ "$http_status" = "200" ]; then
echo "$name ($full_ref): reachable"
else
echo "::warning::$name: manifest returned '${http_status:-no response}' for $full_ref"
warn_count=$((warn_count + 1))
fi
done < <(jq -c '.mcps[]' /tmp/mcps.json)
if [ "$warn_count" -gt 0 ]; then
echo "::warning::$warn_count MCP digest(s) unreachable (non-fatal — may be offline or yanked)"
fi
check-eidolon-repos:
name: Check every Eidolon repo is reachable and EIIS-conformant
runs-on: ubuntu-latest
needs: validate-roster
strategy:
fail-fast: false
matrix:
eidolon: [atlas, spectra, ramza, apivr, vivi, idg, forge, vigil, kupo]
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq
sudo wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- name: Resolve repo for ${{ matrix.eidolon }}
id: resolve
run: |
REPO=$(yq -r ".eidolons[] | select(.name == \"${{ matrix.eidolon }}\") | .source.repo" roster/index.yaml)
STATUS=$(yq -r ".eidolons[] | select(.name == \"${{ matrix.eidolon }}\") | .status" roster/index.yaml)
LATEST=$(yq -r ".eidolons[] | select(.name == \"${{ matrix.eidolon }}\") | .versions.latest" roster/index.yaml)
echo "repo=$REPO" >> "$GITHUB_OUTPUT"
echo "status=$STATUS" >> "$GITHUB_OUTPUT"
echo "latest=$LATEST" >> "$GITHUB_OUTPUT"
echo "tag=v$LATEST" >> "$GITHUB_OUTPUT"
- name: Skip in-construction Eidolons
if: steps.resolve.outputs.status == 'in_construction'
run: echo "::notice::${{ matrix.eidolon }} is in_construction — skipping conformance check"
- name: Clone ${{ matrix.eidolon }}
if: steps.resolve.outputs.status == 'shipped'
run: |
git clone --depth 1 --branch "${{ steps.resolve.outputs.tag }}" \
"https://github.com/${{ steps.resolve.outputs.repo }}" /tmp/eidolon
- name: Verify release integrity metadata
if: steps.resolve.outputs.status == 'shipped'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
name="${{ matrix.eidolon }}"
version="${{ steps.resolve.outputs.latest }}"
meta="$(yq -o=json ".eidolons[] | select(.name == \"$name\") | .versions.releases.\"$version\" // {}" roster/index.yaml)"
if [[ "$meta" == "{}" ]]; then
enforcement="$(yq -r '.integrity.enforcement // "warn"' roster/index.yaml)"
if [[ "$enforcement" == "strict" ]]; then
echo "::error::$name@$version missing release integrity metadata"
exit 1
fi
echo "::warning::$name@$version missing release integrity metadata (compatibility mode)"
exit 0
fi
expected_commit="$(echo "$meta" | jq -r '.commit')"
expected_tree="$(echo "$meta" | jq -r '.tree')"
actual_commit="$(git -C /tmp/eidolon rev-parse HEAD)"
actual_tree="$(git -C /tmp/eidolon rev-parse 'HEAD^{tree}')"
[[ "$actual_commit" == "$expected_commit" ]] || { echo "::error::$name commit mismatch"; exit 1; }
[[ "$actual_tree" == "$expected_tree" ]] || { echo "::error::$name tree mismatch"; exit 1; }
if [[ "$(echo "$meta" | jq -r '.provenance.github_attestation // false')" == "true" ]]; then
# Public Eidolon repos serve release-manifest.json directly via
# GitHub's release CDN — no auth needed. Using curl rather than
# `gh release download` avoids the cross-repo GITHUB_TOKEN scope
# problem (the default token only has access to this repo, not
# to Rynaro/<EIDOLON>; cross-repo gh API calls return HTTP 401).
mkdir -p /tmp/eidolon-release
release_url="https://github.com/${{ steps.resolve.outputs.repo }}/releases/download/v$version/release-manifest.json"
if ! curl -fsSL "$release_url" -o /tmp/eidolon-release/release-manifest.json; then
echo "::error::could not download release-manifest.json from $release_url"
exit 1
fi
# Attestation verify uses Sigstore's public transparency log; it
# tolerates an unauthenticated GH_TOKEN for public attestations.
# If this step starts failing on cross-repo scope, gate it on
# `github.event_name != 'pull_request'` (currently no-op since
# pull_request was dropped from the trigger block above).
gh attestation verify /tmp/eidolon-release/release-manifest.json \
--repo "${{ steps.resolve.outputs.repo }}" \
--signer-workflow "${{ github.repository }}/.github/workflows/eidolon-release-template.yml"
fi
- name: Resolve EIIS tag
if: steps.resolve.outputs.status == 'shipped'
id: eiis
run: |
set -euo pipefail
EIIS_REQ=$(yq -r '.eiis_required' roster/index.yaml)
# Roster pins `eiis_required` as a major.minor compat declaration;
# actual tags are full SemVer. Resolve to the highest matching patch.
if [[ "$EIIS_REQ" =~ ^[0-9]+\.[0-9]+$ ]]; then
EIIS_TAG=$(git ls-remote --tags --refs https://github.com/Rynaro/eidolons-eiis | \
awk -v p="refs/tags/v${EIIS_REQ}." '$2 ~ "^"p { sub("refs/tags/v", "", $2); print $2 }' | \
sort -V | tail -1)
fi
[[ -z "${EIIS_TAG:-}" ]] && EIIS_TAG="$EIIS_REQ"
echo "tag=v$EIIS_TAG" >> "$GITHUB_OUTPUT"
echo "Resolved eiis_required=$EIIS_REQ → tag v$EIIS_TAG"
- name: Clone Rynaro/eidolons-eiis@${{ steps.eiis.outputs.tag }}
if: steps.resolve.outputs.status == 'shipped'
run: |
git clone --depth 1 --branch "${{ steps.eiis.outputs.tag }}" \
"https://github.com/Rynaro/eidolons-eiis" /tmp/eiis
- name: EIIS conformance check (external checker)
if: steps.resolve.outputs.status == 'shipped'
run: |
set +e
bash /tmp/eiis/conformance/check.sh /tmp/eidolon
rc=$?
# Exit-code semantics from Rynaro/eidolons-eiis conformance/check.sh:
# 0 — all green (MUSTs + SHOULDs)
# 2 — MUST violation (hard fail)
# 3 — SHOULD violation (advisory; fail CI to surface drift)
# 4 — warn-only / grandfathered (e.g. missing manifest fixture).
# Per the EIIS-1.1 "warn-only first wave" stance, this is
# not a CI blocker — surface as a warning instead.
case "$rc" in
0)
echo "::notice::${{ matrix.eidolon }} passes EIIS ${{ steps.eiis.outputs.tag }} conformance"
;;
4)
echo "::warning::${{ matrix.eidolon }} passes MUSTs but has grandfathered SHOULD warnings (exit 4)"
;;
*)
echo "::error::${{ matrix.eidolon }} fails EIIS ${{ steps.eiis.outputs.tag }} conformance (exit $rc)"
exit 1
;;
esac
- name: Verify install.sh exits cleanly with --help
if: steps.resolve.outputs.status == 'shipped'
run: |
cd /tmp/eidolon
bash install.sh --help > /dev/null
- name: Verify install.sh --non-interactive succeeds (budget gate)
if: steps.resolve.outputs.status == 'shipped'
run: |
set -euo pipefail
cd /tmp/eidolon
target="$(mktemp -d)/.eidolons/${{ matrix.eidolon }}"
# A full non-interactive install exercises gates that --help and EIIS
# conformance never reach — notably each Eidolon's always-loaded
# agent.md ≤1000-token P0 budget, which install.sh hard-exits 4 on
# when blown. This is the gate that would have caught ATLAS v1.9.0 /
# VIGIL v1.4.0 shipping an over-budget agent.md (the CRYSTALIUM
# memory-mandate regression). --hosts is pinned because the clone has
# no host markers to auto-detect.
if ! bash install.sh --non-interactive --hosts claude-code --target "$target"; then
echo "::error::${{ matrix.eidolon }} install.sh --non-interactive failed (agent.md token budget or install-layout regression)"
exit 1
fi
# CLI smoke testing has moved to .github/workflows/ci.yml (install-e2e +
# cli-tests jobs, which cover every command across Ubuntu and macOS with
# bats). This workflow now focuses on its namesake: verifying the roster
# itself is valid and every upstream Eidolon repo is reachable + EIIS-
# conformant (runs on path-filtered pushes and nightly).
nexus-integrity:
name: Nexus release integrity
runs-on: ubuntu-latest
needs: validate-roster
# Skip if roster nexus block is absent (old roster) — defensive guard for
# forks/PRs that pre-date the nexus: block.
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update -q
sudo apt-get install -y jq
sudo wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- name: Check nexus integrity metadata
run: |
set -euo pipefail
# Read latest nexus version from roster.
latest="$(yq -r '.nexus.versions.latest // ""' roster/index.yaml)"
if [[ -z "$latest" || "$latest" == "null" ]]; then
echo "::notice::nexus: block absent from roster — skipping integrity check (bootstrap window)"
exit 0
fi
echo "Nexus latest: v$latest"
# Read metadata.
commit="$(yq -r ".nexus.versions.releases[\"$latest\"].commit // \"\"" roster/index.yaml)"
# Skip if placeholder (bootstrap window before first release workflow run).
case "$commit" in
"<"*|"")
echo "::notice::nexus@$latest has placeholder commit — skipping integrity check (bootstrap window)"
exit 0
;;
esac
# Clone the tagged nexus.
git clone --depth 1 --branch "v$latest" \
https://github.com/Rynaro/eidolons /tmp/nexus-check
actual_commit="$(git -C /tmp/nexus-check rev-parse HEAD)"
actual_tree="$(git -C /tmp/nexus-check rev-parse 'HEAD^{tree}')"
expected_tree="$(yq -r ".nexus.versions.releases[\"$latest\"].tree // \"\"" roster/index.yaml)"
expected_archive="$(yq -r ".nexus.versions.releases[\"$latest\"].archive_sha256 // \"\"" roster/index.yaml)"
[[ "$actual_commit" == "$commit" ]] \
|| { echo "::error::nexus commit mismatch: got $actual_commit, expected $commit"; exit 1; }
[[ -z "$expected_tree" ]] || [[ "${expected_tree:0:1}" == "<" ]] \
|| [[ "$actual_tree" == "$expected_tree" ]] \
|| { echo "::error::nexus tree mismatch: got $actual_tree, expected $expected_tree"; exit 1; }
if [[ -n "$expected_archive" && "${expected_archive:0:1}" != "<" ]]; then
tmp_tar="$(mktemp)"
git -C /tmp/nexus-check archive --format=tar --prefix="eidolons-${latest}/" HEAD \
| gzip > "$tmp_tar"
# Compare the .tar.gz sha256 (gzip deterministic with consistent settings).
# Note: git archive produces deterministic output; gzip without timestamp matches workflow.
# For now check the raw tar sha256 to match what the workflow records.
git -C /tmp/nexus-check archive --format=tar --prefix="eidolons-${latest}/" HEAD \
> /tmp/nexus.tar
if command -v sha256sum >/dev/null 2>&1; then
actual_archive="$(sha256sum /tmp/nexus.tar | awk '{print $1}')"
else
actual_archive="$(shasum -a 256 /tmp/nexus.tar | awk '{print $1}')"
fi
rm -f /tmp/nexus.tar "$tmp_tar"
[[ "$actual_archive" == "$expected_archive" ]] \
|| { echo "::error::nexus archive sha256 mismatch: got $actual_archive, expected $expected_archive"; exit 1; }
fi
echo "::notice::nexus@$latest integrity verified (commit=$actual_commit)"