A Node.js 24 GitHub Action bundled as dist/index.cjs. The action validates one metadata entity per pull request and has no direct GitHub comment/review write API.
Paths follow {vaults|operators|networks|tokens|adapters}/{Ethereum address}/{info.json|logo.png} or {points|curators}/{kebab-case identifier}/{info.json|logo.png}. Changes come from the GitHub API comparison of the PR merge commit with its first parent, using immutable SHAs. The action verifies the merge commit, PR head and comparison base before accepting up to 100 changed files, including deleted and renamed paths. Each API request has a 10-second deadline covering the response body and an 8 MiB response limit; redirects are rejected. API errors or inconsistent responses fail validation. The validator does not launch Git or a shell. changes-json is available for structured local fixtures; legacy whitespace-separated files remains deprecated.
Filesystem validation runs first. On-chain addresses must be unique within their category regardless of letter casing; unique existing casing is preserved. Surviving entities always require strict JSON info.json (at most 256 KiB) and validate every present logo, including unchanged files. JSON.parse is authoritative; json-source-map supplies line locations only, Ajv checks own properties, and duplicate keys use the last value. Invalid schema data never reaches RPC validation. Full entity deletion is allowed, logo is optional, and symbolic links or special files are rejected.
Optional fields recognized by the metadata service have matching producer types in every applicable schema. Unknown additional fields remain allowed for forward-compatible metadata extensions.
After schema validation, independent registry, logo, collateral and rewards checks collect diagnostics. Logos must be PNG, at most 100 KiB and 256×256, with valid chunk bounds/CRC and a single first IHDR; decoded dimensions are checked too. Vault rewards support defaultStakingRewardsV2 and defaultStakingRewardsV2Epoched. Ordinary rewards use rewards-factory and VAULT(); Epoched rewards use the comma-separated rewards-epoched-factories and i_vault(). Each contract must belong to a configured factory for its own type and reference the current vault. Only types present in metadata require their factory configuration. Missing or invalid factory entries fail validation; registry rejection, vault mismatch and RPC failures remain distinct outcomes.
The PR workflow runs with contents: read and passes its automatic GITHUB_TOKEN through github-token (which defaults to ${{ github.token }}). No write token or repository secret is needed. Check out ${{ github.sha }} explicitly so the files on disk match the immutable comparison; one commit of history is sufficient. Pass pull-request and head-sha from the event. Leave local fixture inputs unset in CI and never populate them from PR text. Upload only the absolute result-file output. Deprecated token and issue inputs are ignored. The action writes annotations, a job summary and a private RUNNER_TEMP/metadata-validation-*/result.json; a broken summary does not suppress the structured file.
Result version 2 contains repository, pullRequest, headSha, runSha (PR merge commit), runId, runAttempt, kind, conclusion and at most 100 public diagnostics. Artifact name is metadata-validation-result-<runId>-<runAttempt>. No raw metadata values enter public diagnostic messages. Outputs also include error-count and error.
A separately pinned trusted reporter runs on workflow_run with actions/contents read and pull-requests write. It checks run/workflow/attempt identity, artifact size and layout, current PR head and merge SHA, repository paths and newer runs before appending a new comment for each validation run attempt. Previously published comments are preserved; redelivery of the same run attempt does not add another result. If the PR changes during delivery, the reporter appends a correction. Locations use Markdown code spans; text from metadata cannot create links or images. A comment is not the merge gate.
Comments use ✅ for success and ❌ for failure, with every problem in a bullet list. Trusted path-policy and result-availability problems are shown alongside artifact diagnostics. At most 50 artifact errors are displayed, with a list item pointing to the remaining errors in the run. Commit/run/attempt identity remains in hidden markers and the run link, without a visible bookkeeping footer.
Configuration-only maintenance PRs use a small explicit allowlist and report kind=maintenance; metadata was not checked and CODEOWNERS approval is required. Mixed configuration/metadata changes fail. See rollout and protection requirements; CODEOWNERS files alone do not enforce review.
The dependency-free extractor checks every category/entity/file using lstat, rejects duplicate addresses within a category, malformed JSON and invalid present logos, then atomically replaces full-info.json. It has 10000-entity and 64 MiB output limits. Logo URLs use the immutable caller SHA. The JSON layout remains compatible with existing consumers.
The release contains full-info.json, full-info.json.sha256 and a version-1 full-info.manifest.json. The manifest has exactly version, repository, workflowId, runId, runNumber, runAttempt, revision, sha256 and bytes. Its digest and byte count bind the exact JSON bytes without reserialization. Identity comes from the caller's trusted GitHub context and the verified run-attempt API, never from metadata. GITHUB_RUN_ATTEMPT supplies the current attempt; the API supplies the numeric workflow ID and verifies .github/workflows/full-info.yml, repository, branch, event, run number and SHA.
Generation uses contents: read and actions: read. Publication uses contents: write and actions: read; the caller must grant these scopes too. Generation places its initial manifest in the artifact. Before publishing, the workflow verifies checksum, manifest origin and current main revision. A publish-only rerun may reuse an earlier generation attempt of the same run/revision, then rewrites the final manifest with the current publish attempt. Other run identities, newer artifact attempts, or different bytes fail validation.
The caller serializes publication; retries replace the same three assets. A bounded streaming check compares the public manifest, actual JSON bytes and checksum before the workflow completes. Each of six attempts has a shared 10-second deadline and limits of 64 MiB for JSON, 4 KiB for the manifest and 1 KiB for the checksum, with 5-second delays between attempts. Assets are separate writes; a successful publisher check cannot prove that every downstream cache sees the same bytes. The consumer independently matches the manifest to the signed successful workflow event and retries transient stale/missing assets automatically. A SHA-256 checksum alone is not authentication.
Publish and verify the producer before deploying the manifest-aware metadata service. Pin each caller to the published implementation revision. See the publication rollout sequence.
Use Node 24 and the exact pnpm version and archive hash in package.json#packageManager. All dependencies come from public npm; ESLint and Prettier rules are stored locally. package.json pins exact direct versions, while pnpm-lock.yaml pins the entire dependency graph with SHA-512 archive integrity hashes. Existing package versions and integrity hashes are preserved.
Always use a frozen install: an archive hash mismatch or an out-of-sync manifest must fail rather than refresh the trusted lockfile. pnpm-workspace.yaml enables frozen installs, exact version saving and store integrity verification. The explicit registry below also overrides any private registry in your user configuration. Lifecycle scripts are disabled during installation; building and testing run explicitly afterwards.
corepack install
pnpm install --frozen-lockfile --ignore-scripts --registry=https://registry.npmjs.org/
pnpm run typecheck
pnpm run lint
pnpm run bundleSource files, workflow scripts and configuration use TypeScript. Node 24 runs workflow scripts directly; dist/index.cjs is the bundled entrypoint required by the GitHub Action. CI checks types, lint and reproducible bundle output. Update source and regenerate dist together.
The local test suite is kept in the ignored test/ directory and is not included in repository checkouts. Where that suite is available, pnpm test builds the action, type-checks the tests and executes them with Node 24.
Dependency hashes prevent replacement of pinned archives; they do not establish that a package was safe when first approved. Review manifest and lockfile changes together. For an intentional dependency update, use pnpm update --no-frozen-lockfile --registry=https://registry.npmjs.org/, inspect all changed versions and hashes, and rerun the checks before accepting the new lockfile. Never refresh a checksum merely to make an integrity failure pass.