test: gate releases on enforced conformance and installed package checks
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
This commit is contained in:
parent
a15fe032b0
commit
1ece969f59
6 changed files with 206 additions and 17 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
# CI smoke — package tests prove hub-core library health on push.
|
# Local source, enforcement, inventory and installed-package gates.
|
||||||
name: CI Smoke
|
name: CI Smoke
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
@ -22,23 +22,21 @@ jobs:
|
||||||
pytest-smoke:
|
pytest-smoke:
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
steps:
|
steps:
|
||||||
- name: Run hub-core pytest
|
- name: Test enforcement, inventory and installed package
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
REF="${GITHUB_SHA:-main}"
|
REF="${GITHUB_SHA:?commit SHA required}"
|
||||||
SHORT="${REF:0:7}"
|
CI_WORKDIR="$(mktemp -d)"
|
||||||
ROOT="${HOME}/ci-hub-core-${SHORT}"
|
trap 'rm -rf -- "$CI_WORKDIR"' EXIT
|
||||||
rm -rf "${ROOT}"
|
wget -qO "$CI_WORKDIR/source.tar.gz" \
|
||||||
mkdir -p "${ROOT}"
|
"https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${REF}.tar.gz"
|
||||||
wget -qO /tmp/hub-core.tar.gz \
|
mkdir "$CI_WORKDIR/source"
|
||||||
"https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz"
|
tar xzf "$CI_WORKDIR/source.tar.gz" -C "$CI_WORKDIR/source" --strip-components=1
|
||||||
tar xzf /tmp/hub-core.tar.gz -C "${ROOT}" --strip-components=1
|
cd "$CI_WORKDIR/source"
|
||||||
cd "${ROOT}"
|
|
||||||
if ! command -v uv >/dev/null 2>&1; then
|
if ! command -v uv >/dev/null 2>&1; then
|
||||||
pip install --user uv
|
pip install --user uv
|
||||||
export PATH="${HOME}/.local/bin:${PATH}"
|
export PATH="${HOME}/.local/bin:${PATH}"
|
||||||
fi
|
fi
|
||||||
uv sync
|
uv sync --locked --group dev --extra runtime
|
||||||
uv run python -c "import hub_core; print(hub_core.__version__)"
|
make ci-check
|
||||||
uv run python -m pytest -q
|
echo "hub-core source, enforcement, inventory and package checks passed @ ${REF}"
|
||||||
echo "hub-core pytest smoke ok @ ${SHORT}"
|
|
||||||
|
|
|
||||||
14
Makefile
14
Makefile
|
|
@ -1,4 +1,4 @@
|
||||||
.PHONY: install test runtime-run conformance container-build ecosystem-regression
|
.PHONY: install test runtime-run conformance container-build ecosystem-regression inventory-check package-check ci-check
|
||||||
|
|
||||||
UV ?= uv
|
UV ?= uv
|
||||||
ECOSYSTEM_REGRESSION ?= /home/worsch/the-custodian/scripts/hub-ecosystem-regression.sh
|
ECOSYSTEM_REGRESSION ?= /home/worsch/the-custodian/scripts/hub-ecosystem-regression.sh
|
||||||
|
|
@ -9,7 +9,7 @@ install:
|
||||||
$(UV) sync
|
$(UV) sync
|
||||||
|
|
||||||
test:
|
test:
|
||||||
$(UV) run python -m pytest -q
|
$(UV) run --locked --extra runtime python -m pytest -q
|
||||||
|
|
||||||
runtime-run:
|
runtime-run:
|
||||||
$(UV) run --extra runtime hub-core api
|
$(UV) run --extra runtime hub-core api
|
||||||
|
|
@ -22,3 +22,13 @@ container-build:
|
||||||
|
|
||||||
ecosystem-regression:
|
ecosystem-regression:
|
||||||
bash $(ECOSYSTEM_REGRESSION)
|
bash $(ECOSYSTEM_REGRESSION)
|
||||||
|
|
||||||
|
# Local equivalents of the required CI gates.
|
||||||
|
inventory-check:
|
||||||
|
$(UV) run --locked --extra runtime python tools/build_access_inventory.py --inventory docs/platform-access-inventory.json --check
|
||||||
|
|
||||||
|
package-check:
|
||||||
|
$(UV) build --out-dir dist/ci
|
||||||
|
@set -eu; set -- dist/ci/*.whl; test "$$#" -eq 1; $(UV) run --no-project --with "$$1" python -I tools/check_installed_package.py
|
||||||
|
|
||||||
|
ci-check: test inventory-check package-check
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,27 @@ absorption slices that are not part of the T04 minimal vertical. Tenant
|
||||||
isolation also remains open because the 0.1 runtime has no tenant identity or
|
isolation also remains open because the 0.1 runtime has no tenant identity or
|
||||||
authorization context yet. These gaps must not be interpreted as passing; the
|
authorization context yet. These gaps must not be interpreted as passing; the
|
||||||
harness reports only the implemented profile above.
|
harness reports only the implemented profile above.
|
||||||
|
|
||||||
|
## Access enforcement and CI gates
|
||||||
|
|
||||||
|
`make ci-check` runs the test suite, checks the reviewed access inventory for
|
||||||
|
source drift, builds distributions and validates an installed wheel outside the
|
||||||
|
checkout's import path. Forgejo runs these gates for `main` pushes and manual
|
||||||
|
runs, using the full commit SHA and a unique temporary checkout. CI installs the
|
||||||
|
locked development and runtime dependencies first. Individual gates are
|
||||||
|
`make test`, `make inventory-check` and `make package-check`.
|
||||||
|
|
||||||
|
`tests/test_enforced_conformance.py` runs all twelve existing Tier 2/3 checks
|
||||||
|
through an explicitly enforced runtime using a real signed IAM JWT and synthetic
|
||||||
|
owner facts, policy and audit. It verifies event attribution against authorization
|
||||||
|
records. Additional journeys establish valid state, deny both reads and writes
|
||||||
|
for anonymous/invalid credentials, revoked entitlement, policy denial and
|
||||||
|
policy/audit outages, then independently read back unchanged stored messages.
|
||||||
|
These tests run in the ordinary suite; they need no external owner checkout.
|
||||||
|
|
||||||
|
The installed-wheel gate validates runtime/security imports, packaged action and
|
||||||
|
browser route coverage, contract fixtures/schemas and the migration template.
|
||||||
|
It makes no owner requests and starts no service. The separate optional Audit
|
||||||
|
Core interoperability suite still requires `HUB_CORE_AUDIT_CORE_SOURCE` and is
|
||||||
|
not silently represented as covered by ordinary CI. Local CI-equivalent success
|
||||||
|
is not a deployed Forgejo receipt or live owner/platform acceptance.
|
||||||
|
|
|
||||||
89
tests/test_enforced_conformance.py
Normal file
89
tests/test_enforced_conformance.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
"""Disposable enforcement-mode journeys; synthetic owners, real signed IAM token.
|
||||||
|
|
||||||
|
These exercise the complete Tier 2/3 workload through AccessBoundary. They do
|
||||||
|
not establish issuer registration, deployed custody or platform acceptance.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from dataclasses import replace
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from hub_core.conformance import ConformanceHarness
|
||||||
|
from hub_core.runtime.app import create_app
|
||||||
|
from hub_core.runtime.config import RuntimeSettings
|
||||||
|
from test_access_boundary import Owners
|
||||||
|
from test_access_identity import setup
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='module')
|
||||||
|
def signing_key():
|
||||||
|
return rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def enforced(signing_key):
|
||||||
|
token, identity, _, upstream = setup(signing_key)
|
||||||
|
owners = Owners()
|
||||||
|
owners.facts = replace(owners.facts, producer_addresses=frozenset({'hub:ops-hub'}))
|
||||||
|
async def current_facts(actor, resource):
|
||||||
|
return replace(owners.facts, checked_at=time.time())
|
||||||
|
owners.resolve = current_facts
|
||||||
|
controller = owners.controller()
|
||||||
|
controller.identity = identity
|
||||||
|
app = create_app(settings=RuntimeSettings(environment='test', access_mode='enforce'),
|
||||||
|
access_controller=controller)
|
||||||
|
with TestClient(app, base_url='https://hub.example', headers={'Authorization': 'Bearer ' + token}) as client:
|
||||||
|
yield client, owners
|
||||||
|
asyncio.run(upstream.aclose())
|
||||||
|
|
||||||
|
|
||||||
|
def test_tier_2_and_3_workload_passes_through_enforcement(enforced):
|
||||||
|
client, owners = enforced
|
||||||
|
report = ConformanceHarness(client).run()
|
||||||
|
assert report.passed, report.to_dict()
|
||||||
|
assert report.passed_count == 12
|
||||||
|
assert owners.requests
|
||||||
|
assert all(r.actor.subject == 'immutable-root' for r in owners.requests)
|
||||||
|
records = {r['correlation_id']: r for r in owners.records if r['outcome'] == 'authorized'}
|
||||||
|
for family in ('progress', 'interaction'):
|
||||||
|
result = client.get('/ports/projections/' + family + '_events')
|
||||||
|
event = result.json()['data']['items'][0]
|
||||||
|
attribution = event['payload']['_hub_access']
|
||||||
|
record = records[attribution['correlation_id']]
|
||||||
|
assert record['subject'] == 'immutable-root'
|
||||||
|
assert 'Projection rebuild conformance fixture.' not in json.dumps(owners.records)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('failure,status', [('anonymous',401), ('invalid',401),
|
||||||
|
('revoked',403), ('policy_denied',403), ('policy_outage',503), ('audit_outage',503)])
|
||||||
|
def test_denied_reads_and_writes_leave_no_business_mutation(enforced, failure, status):
|
||||||
|
client, owners = enforced
|
||||||
|
assert ConformanceHarness(client).run().passed
|
||||||
|
before = client.get('/ports/projections/messages').json()['data']['items']
|
||||||
|
saved = client.headers['authorization']
|
||||||
|
if failure == 'anonymous':
|
||||||
|
del client.headers['authorization']
|
||||||
|
elif failure == 'invalid':
|
||||||
|
client.headers['authorization'] = 'Bearer invalid'
|
||||||
|
elif failure == 'revoked':
|
||||||
|
owners.facts = replace(owners.facts, root_entitled=False)
|
||||||
|
elif failure == 'policy_denied':
|
||||||
|
owners.allow = False
|
||||||
|
elif failure == 'policy_outage':
|
||||||
|
owners.policy_down = True
|
||||||
|
else:
|
||||||
|
owners.audit_down = True
|
||||||
|
message = {'schema_version':'0.1.0', 'correlation_id':str(uuid4()),
|
||||||
|
'from_address':'hub:ops-hub', 'to_addresses':['agent:conformance'],
|
||||||
|
'body':'must never commit'}
|
||||||
|
assert client.get('/ports/projections/messages').status_code == status
|
||||||
|
assert client.post('/ports/messaging/messages', json=message).status_code == status
|
||||||
|
client.headers['authorization'] = saved
|
||||||
|
owners.facts = replace(owners.facts, root_entitled=True)
|
||||||
|
owners.allow, owners.policy_down, owners.audit_down = True, False, False
|
||||||
|
assert client.get('/ports/projections/messages').json()['data']['items'] == before
|
||||||
46
tools/check_installed_package.py
Normal file
46
tools/check_installed_package.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check an installed wheel outside the checkout's import path (use python -I).
|
||||||
|
|
||||||
|
Run via: uv run --no-project --with dist/<wheel> python -I tools/check_installed_package.py
|
||||||
|
No startup, owner requests, credentials or database connection are needed.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from importlib.resources import files
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import hub_core
|
||||||
|
from hub_core.conformance import ConformanceHarness
|
||||||
|
from hub_core.runtime.app import create_app
|
||||||
|
from hub_core.runtime.config import RuntimeSettings
|
||||||
|
from hub_core.runtime.inbox_projection import create_inbox_projection_router
|
||||||
|
from hub_core.security.boundary import iter_routes, route_key
|
||||||
|
from hub_core.security.browser import BROWSER_ROUTES, create_browser_router
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
checkout = Path(__file__).resolve().parents[1]
|
||||||
|
installed = Path(hub_core.__file__).resolve()
|
||||||
|
if installed.is_relative_to(checkout):
|
||||||
|
raise RuntimeError('package smoke imported checkout instead of installed wheel')
|
||||||
|
catalog = json.loads(files('hub_core.security').joinpath('routes.json').read_text())['routes']
|
||||||
|
app = create_app(settings=RuntimeSettings(environment='test', access_mode='enforce'))
|
||||||
|
routes = [*iter_routes(app), *iter_routes(create_inbox_projection_router())]
|
||||||
|
missing = [route_key(route, method) for route in routes if hasattr(route, 'methods')
|
||||||
|
for method in route.methods if route.path != '/healthz' and route_key(route, method) not in catalog]
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError('installed action catalog is incomplete: ' + ', '.join(missing))
|
||||||
|
browser = {(method, route.path) for route in iter_routes(create_browser_router())
|
||||||
|
for method in route.methods}
|
||||||
|
if browser != BROWSER_ROUTES:
|
||||||
|
raise RuntimeError('installed browser route coverage differs')
|
||||||
|
# Constructor loads packaged contract fixtures; these checks load schemas.
|
||||||
|
harness = ConformanceHarness(None)
|
||||||
|
harness._schema_validate()
|
||||||
|
harness._no_secrets()
|
||||||
|
if not files('hub_core.migrations').joinpath('script.py.mako').is_file():
|
||||||
|
raise RuntimeError('installed migration template missing')
|
||||||
|
print('Installed wheel: imports, action catalog, browser routes, contract resources and migration template pass')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
@ -325,6 +325,28 @@ T01–T04 remain `progress`; issuer registration/MFA, owner-facts composition,
|
||||||
MCP consumer adoption, operation-outcome auditing and platform conformance remain
|
MCP consumer adoption, operation-outcome auditing and platform conformance remain
|
||||||
open. No production listener or entitlement changed.
|
open. No production listener or entitlement changed.
|
||||||
|
|
||||||
|
## Conformance and CI continuation — 2026-09-28
|
||||||
|
|
||||||
|
Added enforcement-mode Tier 2/3 conformance journeys with signed IAM tokens and
|
||||||
|
explicit synthetic owners. Denial/revocation/outage cases exercise reads and
|
||||||
|
writes and verify unchanged business state after restoring access. Event
|
||||||
|
attribution is joined to recorded authorization decisions.
|
||||||
|
|
||||||
|
Forgejo now runs `make ci-check`: the complete ordinary test suite, access
|
||||||
|
inventory drift checks, distribution builds and an isolated installed-wheel
|
||||||
|
resource/import check. CI checks out the full commit into a unique temporary
|
||||||
|
directory and installs locked development/runtime dependencies. The optional
|
||||||
|
owner-source interoperability suite remains separately identified.
|
||||||
|
|
||||||
|
See [conformance documentation](../docs/conformance.md#access-enforcement-and-ci-gates).
|
||||||
|
Validation: local `make ci-check` passed with **330 tests**, one explicitly
|
||||||
|
optional owner-source module skipped, inventory coverage (165 Hub surfaces),
|
||||||
|
distribution builds and installed-wheel checks. Final locked runtime dependency
|
||||||
|
sync and workflow YAML/shell syntax checks also pass.
|
||||||
|
|
||||||
|
These are local source/release gates, not live platform or remote CI acceptance;
|
||||||
|
T01–T04 remain `progress` and T06 remains open.
|
||||||
|
|
||||||
## Acceptance checkpoints
|
## Acceptance checkpoints
|
||||||
|
|
||||||
- [x] Architecture/source/runtime review captured; new implementation owner is hub-core
|
- [x] Architecture/source/runtime review captured; new implementation owner is hub-core
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue