chore(portability): SELinux tonberry template :z default + sha256sum … #717
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)" | |
| verify-ecl-pin: | |
| name: Verify composition ECL pin | |
| runs-on: ubuntu-latest | |
| # Nightly belt-and-suspenders for the composition-drift contract. The | |
| # dedicated composition-drift.yml gate only fires when composition.md / | |
| # its template / that workflow change; it cannot catch a pinned ECL tag | |
| # that is later moved or deleted upstream, nor silent drift on nights when | |
| # nothing in the nexus touched composition.md. This job re-runs the same | |
| # regeneration on the nightly cron so the pin (currently v2.1.1) stays | |
| # honest against Rynaro/eidolons-ecl. | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Read the pinned ECL tag from composition-drift.yml | |
| id: pin | |
| run: | | |
| set -euo pipefail | |
| tag="$(grep -oE 'ECL_PINNED_TAG:[[:space:]]*v[0-9]+\.[0-9]+\.[0-9]+' \ | |
| .github/workflows/composition-drift.yml | awk '{print $2}')" | |
| [[ -n "$tag" ]] || { echo "::error::could not read ECL_PINNED_TAG from composition-drift.yml"; exit 1; } | |
| echo "tag=$tag" >> "$GITHUB_OUTPUT" | |
| echo "Pinned ECL tag: $tag" | |
| - name: Verify the pinned tag still exists upstream | |
| run: | | |
| set -euo pipefail | |
| tag="${{ steps.pin.outputs.tag }}" | |
| if ! git ls-remote --tags https://github.com/Rynaro/eidolons-ecl "refs/tags/${tag}" \ | |
| | grep -q "refs/tags/${tag}"; then | |
| echo "::error::pinned ECL tag ${tag} no longer resolves on Rynaro/eidolons-ecl (moved or deleted)." | |
| exit 1 | |
| fi | |
| echo "::notice::ECL pin ${tag} is reachable upstream." | |
| - name: Clone eidolons-ecl at the pinned tag | |
| run: | | |
| git clone --depth 1 --branch "${{ steps.pin.outputs.tag }}" \ | |
| https://github.com/Rynaro/eidolons-ecl /tmp/ecl | |
| - name: Install eidolons-ecl-sdk from the pinned source | |
| run: pip install --quiet /tmp/ecl/reference-sdk/py | |
| - name: Verify local template matches the pinned ecl template | |
| run: | | |
| diff -u \ | |
| /tmp/ecl/reference-sdk/py/src/eidolons_ecl/compose_gen/templates/composition.md.j2 \ | |
| methodology/composition.md.j2 \ | |
| || { | |
| echo "::error::methodology/composition.md.j2 drifted from pinned eidolons-ecl@${{ steps.pin.outputs.tag }}." | |
| exit 1 | |
| } | |
| - name: Regenerate and diff composition.md against the pinned contracts | |
| run: | | |
| eidolons-ecl compose-gen \ | |
| --contracts /tmp/ecl/contracts \ | |
| --template methodology/composition.md.j2 \ | |
| --out /tmp/regen.md | |
| diff -u methodology/composition.md /tmp/regen.md \ | |
| || { | |
| echo "::error::methodology/composition.md drifted from the regenerated output for pinned tag ${{ steps.pin.outputs.tag }}." | |
| exit 1 | |
| } | |
| echo "::notice::composition.md matches eidolons-ecl@${{ steps.pin.outputs.tag }} (pin verified)." |