Files
molecule-ai-workspace-templ…/.gitea/workflows/verify-providers-projection.yml
T
hongming f5fe7d975a feat(providers): Path B registry-projection artifact + drift gate (internal#718 P4)
Per the codegen-deferred follow-up (internal#718 issue-comment 52157)
the CTO selected Path B: ship a registry-projection artifact alongside
the hand-authored providers/models block, with a subset-relation gate,
WITHOUT retiring or codegen'ing the hand-authored block. This preserves
the federation contract.

Added (all additive; zero runtime behavior change):
  * .gitea/workflows/sync-providers-yaml.yml
      Mirrors molecule-core's cross-repo canonical-sync gate. Fetches
      controlplane internal/providers/providers.yaml via /raw (Gitea
      1.22.6 quirk; /contents returns JSON+base64 envelope) and byte-
      compares against the local synced copy.
  * internal/providers/providers.yaml
      Synced copy of the canonical registry. NOT hand-edited; the sync
      workflow keeps it in step with controlplane main.
  * internal/providers/registry_projection.py
      Generator + verifier. Reads the synced providers.yaml, extracts
      the registry's view of THIS template's runtime, and emits the
      projection artifact. Also runs the SUBSET assertion: every
      (provider, model) the registry claims this runtime serves must
      be servable by the template's hand-authored authoritative source
      (top-level `providers:` block for claude-code/hermes/codex;
      `runtime_config.models[]` for openclaw; top-level `models[]` for
      langgraph). Federation fail-open for runtimes absent from the
      registry (langgraph today).
  * internal/providers/registry-projection.json
      Checked-in projection artifact for this template's runtime.
      Auto-regenerated; the verify gate fails RED on drift.
  * internal/providers/README.md
      Documents the informational status + the subset contract.
  * .gitea/workflows/verify-providers-projection.yml
      Regenerates the artifact in CI and asserts (1) byte-identity
      against the checked-in copy and (2) registry ⊆ template
      hand-authored block.
  * tests/test_registry_projection.py
      TDD coverage: positive (current state is a subset or xfailed
      with the known violation list), negative (injected fake
      (provider, model) pair MUST be rejected — gate has teeth).

The hand-authored block stays AUTHORITATIVE. This gate only stops the
registry from claiming the template serves something it doesn't.

Not regressed (verified):
  * cp#362 — untouched, no controlplane code edited
  * P1 ResolveUpstream — untouched, no proxy code edited
  * P2-B billing — untouched, no billing code edited
  * P3 — untouched, no canvas/templates-from-registry code edited
  * P4 PR-2 (422 hard-reject) — untouched, no workspace-server validation edited
  * P4 closure follow-up (LLM_PROVIDER removed) — untouched, no LLM_PROVIDER read/write
  * Template runtime behavior — untouched, no adapter / config.yaml `providers:`
    / `runtime_config` / `models[]` edited

internal#718 cross-link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:59:03 -07:00

117 lines
4.8 KiB
YAML

name: verify-providers-projection
# Registry-projection drift + subset gate (internal#718 P4 Path B,
# CTO 2026-05-27 "scope-trimmed; preserve federation contract").
#
# This is the molecule-core verify-providers-gen analogue for workspace
# templates. Two invariants:
#
# 1. ARTIFACT DRIFT: regenerate registry-projection.json from the synced
# providers.yaml in memory; RED if it differs from the checked-in
# artifact. Catches a synced-providers.yaml update without a paired
# `registry_projection.py generate` run, AND catches a hand-edit of
# the artifact (it carries a DO NOT EDIT header).
#
# 2. SUBSET RELATION: for this template's runtime, every (provider,
# model) pair the registry claims must also be servable by the
# template's hand-authored authoritative source (top-level `providers:`
# block for claude-code/hermes/codex; runtime_config.models[] for
# openclaw; top-level models[] for langgraph). RED if the registry
# over-claims. Fails OPEN if this runtime is not in the registry at
# all (federation contract — mirrors molecule-ci
# `validate-workspace-template.py` `check_full_providers_block`).
#
# The hand-authored block stays AUTHORITATIVE. This gate only stops the
# registry from claiming the template serves something it doesn't.
#
# ENFORCEMENT POSTURE (matches molecule-core verify-providers-gen):
# standalone workflow; NOT in branch protection initially. Soak first;
# promote to required check in a follow-up once the registry data
# stabilizes (especially the claude-code colon-form non-anthropic pairs
# flagged in the Path B PR body).
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main]
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
permissions:
contents: read
concurrency:
group: verify-providers-projection-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
name: Regenerate projection, fail on drift, assert registry ⊆ template
runs-on: ubuntu-latest
timeout-minutes: 6
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.11'
- name: Install deps
run: |
set -euo pipefail
python -m pip install --quiet --upgrade pip
python -m pip install --quiet pyyaml
- name: Read this template's runtime id from config.yaml
id: rt
run: |
set -euo pipefail
RT=$(python -c "import yaml,sys;print(yaml.safe_load(open('config.yaml')).get('runtime',''))")
if [ -z "$RT" ]; then
echo "::error::config.yaml has no 'runtime:' field — projection gate cannot run."
exit 1
fi
echo "runtime=$RT" >> "$GITHUB_OUTPUT"
echo "Resolved template runtime: $RT"
- name: Verify projection artifact is in sync with the synced providers.yaml
run: |
set -euo pipefail
python internal/providers/registry_projection.py verify "${{ steps.rt.outputs.runtime }}"
- name: Belt-and-braces — regenerate in place and assert clean tree
run: |
set -euo pipefail
python internal/providers/registry_projection.py generate "${{ steps.rt.outputs.runtime }}"
if ! git diff --quiet -- internal/providers/registry-projection.json; then
echo "::error::internal/providers/registry-projection.json drifted from regeneration."
echo "Run 'python internal/providers/registry_projection.py generate ${{ steps.rt.outputs.runtime }}' and commit the result."
git --no-pager diff -- internal/providers/registry-projection.json | head -120
exit 1
fi
echo "OK — projection artifact is in sync."
- name: Subset assertion — registry ⊆ template hand-authored block
run: |
set -euo pipefail
python internal/providers/registry_projection.py subset "${{ steps.rt.outputs.runtime }}"
- name: Unit tests for the projection module
run: |
set -euo pipefail
python -m pip install --quiet pytest
# --rootdir=tests pins pytest's collection root to tests/ so a
# repo-root __init__.py that imports adapter.py (which depends
# on the in-container molecule_runtime wheel, absent on the
# gate runner) does not get auto-collected. This isolates the
# projection-gate tests from the wider in-container test
# surface — the only thing this gate needs to verify is the
# projection module itself.
pytest -q tests/test_registry_projection.py \
--rootdir=tests \
--import-mode=importlib \
-p no:cacheprovider