foundry-testing-actor 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foundry_testing_actor-0.1.0/.github/workflows/ci.yml +140 -0
- foundry_testing_actor-0.1.0/.github/workflows/release.yml +216 -0
- foundry_testing_actor-0.1.0/.gitignore +6 -0
- foundry_testing_actor-0.1.0/CLAUDE.md +98 -0
- foundry_testing_actor-0.1.0/PKG-INFO +258 -0
- foundry_testing_actor-0.1.0/README.md +235 -0
- foundry_testing_actor-0.1.0/adr/ADR-FTA-0001-the-machinery-leaves-the-capability.md +95 -0
- foundry_testing_actor-0.1.0/adr/ADR-FTA-0002-this-actors-half-of-the-three-amigos-round.md +82 -0
- foundry_testing_actor-0.1.0/adr/README.md +24 -0
- foundry_testing_actor-0.1.0/adr/template.md +28 -0
- foundry_testing_actor-0.1.0/docker/Dockerfile +111 -0
- foundry_testing_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-testing/Dockerfile +42 -0
- foundry_testing_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-testing/actor-agentic-context.yaml +48 -0
- foundry_testing_actor-0.1.0/examples/README.md +123 -0
- foundry_testing_actor-0.1.0/pyproject.toml +52 -0
- foundry_testing_actor-0.1.0/scripts/probe_grounding.py +67 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/__init__.py +56 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/cards/actor-data.yaml +68 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/cards/actor-message.yaml +32 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/cards/actor-synchronous-messaging.yaml +59 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/cards/actor.yaml +19 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/cli.py +181 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/config.py +525 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/conformance.py +133 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/correlation.py +193 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/engine.py +889 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/grounding.py +206 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/handler.py +248 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/instance.py +129 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/runner/Dockerfile +32 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/schemas/agentic-context.schema.yaml +168 -0
- foundry_testing_actor-0.1.0/src/foundry_testing_actor/serve.py +129 -0
- foundry_testing_actor-0.1.0/tests/conftest.py +126 -0
- foundry_testing_actor-0.1.0/tests/fixtures/broken/actor-agentic-context.yaml +17 -0
- foundry_testing_actor-0.1.0/tests/test_cards.py +138 -0
- foundry_testing_actor-0.1.0/tests/test_cli.py +68 -0
- foundry_testing_actor-0.1.0/tests/test_config.py +260 -0
- foundry_testing_actor-0.1.0/tests/test_conformance.py +165 -0
- foundry_testing_actor-0.1.0/tests/test_engine.py +527 -0
- foundry_testing_actor-0.1.0/tests/test_grounding.py +155 -0
- foundry_testing_actor-0.1.0/tests/test_handler.py +205 -0
- foundry_testing_actor-0.1.0/tests/test_instance.py +136 -0
- foundry_testing_actor-0.1.0/tests/test_portability.py +79 -0
- foundry_testing_actor-0.1.0/uv.lock +396 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
|
|
4
|
+
# NO CREDENTIAL. Nothing here authenticates to anything: no lab repo, no knowledge registry, no
|
|
5
|
+
# PyPI upload, no image push. Everything this pipeline asserts about THIS package, it asserts from
|
|
6
|
+
# source and from artifacts it built itself — the wheel it just built, and an image built from
|
|
7
|
+
# that wheel rather than from a published one.
|
|
8
|
+
#
|
|
9
|
+
# It does reach the network for inputs: public base images and the npm package the engine drives.
|
|
10
|
+
# That is fetching, not authenticating, and the distinction is the one that matters here — no
|
|
11
|
+
# secret is available to this workflow, so nothing it runs can reach a real capability's repo.
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
|
|
20
|
+
- name: the test suite
|
|
21
|
+
run: uv run --extra dev pytest -q
|
|
22
|
+
|
|
23
|
+
- name: build
|
|
24
|
+
run: uv build
|
|
25
|
+
|
|
26
|
+
# ── the two gates a contract-shipping package runs against its own artifact ────────────
|
|
27
|
+
#
|
|
28
|
+
# The suite above proves the code works in a source checkout. Neither of these does — they
|
|
29
|
+
# prove the WHEEL works, which is a different claim and the one a consumer actually depends
|
|
30
|
+
# on. A schema left out of the build passes every test in tests/ and fails here.
|
|
31
|
+
|
|
32
|
+
- name: the wheel must carry its contract
|
|
33
|
+
run: |
|
|
34
|
+
uv venv /tmp/probe
|
|
35
|
+
uv pip install --python /tmp/probe/bin/python -q dist/*.whl
|
|
36
|
+
/tmp/probe/bin/foundry-testing-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-testing
|
|
37
|
+
/tmp/probe/bin/foundry-testing-actor show examples/ACME.PARTS.CAP.SUP.007.WID-testing \
|
|
38
|
+
--registry reg.example.com | tee /tmp/show.txt
|
|
39
|
+
grep -q 'reg.example.com/acme.parts/sup.007.wid/backend/tests:<version>' /tmp/show.txt
|
|
40
|
+
grep -q 'reg.example.com/acme.parts/sup.007.wid/backend:<version>' /tmp/show.txt
|
|
41
|
+
|
|
42
|
+
- name: the wheel must carry the actor's own cards
|
|
43
|
+
# The `-actor` suffix asserts a papeete-actor underneath, which lint-card can check
|
|
44
|
+
# (ADR-ECO-0022). The suite proves the cards are conformant in a source checkout; this
|
|
45
|
+
# proves they SHIPPED. A card folder left out of the build passes every test in tests/ and
|
|
46
|
+
# makes the name a claim the artifact cannot honour. `papeete-actor-synchronous-messaging`
|
|
47
|
+
# is already a runtime dependency, so its gate is in the probe venv with nothing to add.
|
|
48
|
+
run: |
|
|
49
|
+
/tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
|
|
50
|
+
"$(/tmp/probe/bin/python -c 'from foundry_testing_actor import cards_path; print(cards_path())')"
|
|
51
|
+
|
|
52
|
+
- name: the wheel must carry the default runner
|
|
53
|
+
# A component that declares no `runner:` is built with the Dockerfile the wheel ships.
|
|
54
|
+
# A wheel without it lints every sidecar green and fails at the first publish — after a
|
|
55
|
+
# commit and a push have already landed.
|
|
56
|
+
run: |
|
|
57
|
+
/tmp/probe/bin/python -c 'from foundry_testing_actor import runner_path; p = runner_path() / "Dockerfile"; assert p.is_file(), p; print(p)'
|
|
58
|
+
|
|
59
|
+
- name: the gate must run
|
|
60
|
+
# A gate that cannot fail is not a gate. Lint a deliberately non-conformant sidecar with
|
|
61
|
+
# the INSTALLED wheel and require a non-zero exit.
|
|
62
|
+
run: |
|
|
63
|
+
if /tmp/probe/bin/foundry-testing-actor lint tests/fixtures/broken; then
|
|
64
|
+
echo "::error::lint accepted a non-conformant sidecar — the gate is not running"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
echo "the gate refused a non-conformant sidecar, as it should"
|
|
68
|
+
|
|
69
|
+
- name: grounding renders a CLAUDE.md whose imports resolve
|
|
70
|
+
# The property the whole design rests on: an @-import naming a file that is not there is a
|
|
71
|
+
# session grounded in nothing, and it looks identical to one grounded correctly.
|
|
72
|
+
run: /tmp/probe/bin/python scripts/probe_grounding.py examples/ACME.PARTS.CAP.SUP.007.WID-testing
|
|
73
|
+
|
|
74
|
+
# ── the other half of what this package ships ─────────────────────────────────────────────────
|
|
75
|
+
#
|
|
76
|
+
# ADR-FTA-0001 (after ADR-FIA-0005): a use is a sidecar, and the image is what turns one into a
|
|
77
|
+
# running actor. The job above proves the WHEEL works. This one proves the claim a consumer actually depends on —
|
|
78
|
+
# that a folder containing one YAML file builds into an actor that boots and answers its doors.
|
|
79
|
+
# Nothing here reaches a registry: the image is built, run and thrown away in the runner.
|
|
80
|
+
image:
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
steps:
|
|
83
|
+
- uses: actions/checkout@v4
|
|
84
|
+
- uses: astral-sh/setup-uv@v5
|
|
85
|
+
|
|
86
|
+
- name: build the wheel this image will carry
|
|
87
|
+
# The image installs the wheel from dist/ rather than from PyPI, so that an image tagged
|
|
88
|
+
# <version> cannot contain some other build of <version> — see docker/Dockerfile.
|
|
89
|
+
run: uv build
|
|
90
|
+
|
|
91
|
+
- name: the base image
|
|
92
|
+
run: docker build -f docker/Dockerfile -t foundry-testing-actor:ci .
|
|
93
|
+
|
|
94
|
+
- name: the image and the wheel are one release
|
|
95
|
+
# Two artifacts, one version. An image whose wheel says something else is the failure this
|
|
96
|
+
# arrangement creates, so it is the one CI asserts.
|
|
97
|
+
run: |
|
|
98
|
+
wheel="$(ls dist/*.whl | sed -E 's/.*-([0-9][^-]*)-py3.*/\1/')"
|
|
99
|
+
in_image="$(docker run --rm --entrypoint python foundry-testing-actor:ci \
|
|
100
|
+
-c 'from foundry_testing_actor import version; print(version())')"
|
|
101
|
+
echo "wheel=$wheel image=$in_image"
|
|
102
|
+
[ "$wheel" = "$in_image" ] || { echo "::error::image carries $in_image, wheel is $wheel"; exit 1; }
|
|
103
|
+
|
|
104
|
+
- name: a use is a sidecar
|
|
105
|
+
# The example folder holds exactly one file besides its Dockerfile. If this builds, the
|
|
106
|
+
# claim in the README is true of a real folder rather than of prose.
|
|
107
|
+
run: |
|
|
108
|
+
test "$(ls examples/ACME.PARTS.CAP.SUP.007.WID-testing | grep -cv '^Dockerfile$')" = "1"
|
|
109
|
+
docker build --build-arg ACTOR_IMAGE=foundry-testing-actor:ci \
|
|
110
|
+
-t use:ci examples/ACME.PARTS.CAP.SUP.007.WID-testing
|
|
111
|
+
|
|
112
|
+
- name: the rendered cards are what a caller is validated against
|
|
113
|
+
# Rendering is the construction that replaces the hand copy conformance.check was built to
|
|
114
|
+
# police. Assert it inside the image, against the cards the image actually runs.
|
|
115
|
+
run: |
|
|
116
|
+
docker run --rm use:ci foundry-testing-actor lint /actor
|
|
117
|
+
|
|
118
|
+
- name: it boots, and answers the doors its cards declare
|
|
119
|
+
# The credentials are deliberately junk. The engine refuses to construct without a
|
|
120
|
+
# GITHUB_TOKEN — which is itself worth having, and is why they are set rather than omitted
|
|
121
|
+
# — and no door is actually exercised here: a 400 means routed and refused the payload, a
|
|
122
|
+
# 404 would mean the route was never built.
|
|
123
|
+
run: |
|
|
124
|
+
docker run -d --name smoke -p 18080:8080 \
|
|
125
|
+
-e GITHUB_TOKEN=ci -e CLAUDE_CODE_OAUTH_TOKEN=ci use:ci
|
|
126
|
+
for _ in $(seq 1 30); do
|
|
127
|
+
curl -fsS -m 2 http://localhost:18080/health >/dev/null 2>&1 && break
|
|
128
|
+
sleep 1
|
|
129
|
+
done
|
|
130
|
+
curl -fsS -m 5 http://localhost:18080/health | grep -q ok
|
|
131
|
+
for door in test-task propose-acceptance; do
|
|
132
|
+
code="$(curl -sS -o /dev/null -w '%{http_code}' -m 5 -X POST \
|
|
133
|
+
-H 'content-type: application/json' -d '{}' "http://localhost:18080/$door")"
|
|
134
|
+
[ "$code" = "400" ] || { echo "::error::POST /$door answered $code, expected 400 (routed, bad payload)"; exit 1; }
|
|
135
|
+
done
|
|
136
|
+
code="$(curl -sS -o /dev/null -w '%{http_code}' -m 5 -X POST \
|
|
137
|
+
-H 'content-type: application/json' -d '{}' http://localhost:18080/no-such-door)"
|
|
138
|
+
[ "$code" = "404" ] || { echo "::error::an undeclared door answered $code, expected 404"; exit 1; }
|
|
139
|
+
docker logs smoke | grep -q actor-started
|
|
140
|
+
docker rm -f smoke
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ["v*"]
|
|
5
|
+
|
|
6
|
+
# A REGISTRY ADDED AFTER A TAG WAS ALREADY CUT. The image job below publishes to two registries,
|
|
7
|
+
# and the second one was not always there. Running this workflow by hand backfills the image for
|
|
8
|
+
# a version whose wheel is already on PyPI — without inventing a version number whose only
|
|
9
|
+
# content is "the last release missed a registry".
|
|
10
|
+
#
|
|
11
|
+
# THE TAG IS AN INPUT, NOT THE REF THIS RUNS FROM. GitHub takes the workflow FILE from the ref it
|
|
12
|
+
# is dispatched on, and the tag needing a backfill is by definition older than the workflow that
|
|
13
|
+
# can do it — dispatching on that tag would run the very file that lacks this trigger. So run it
|
|
14
|
+
# from the default branch and name the tag here; the checkout below takes the CONTENT from the
|
|
15
|
+
# tag, and the version gate still refuses a tag that disagrees with the `pyproject.toml` beside
|
|
16
|
+
# it.
|
|
17
|
+
workflow_dispatch:
|
|
18
|
+
inputs:
|
|
19
|
+
tag:
|
|
20
|
+
description: "Existing tag to publish an image for, e.g. v0.1.0"
|
|
21
|
+
required: true
|
|
22
|
+
|
|
23
|
+
# PyPI Trusted Publishing (OIDC) — no token is stored anywhere. The one-time setup is a pending
|
|
24
|
+
# publisher on pypi.org naming this repo and this workflow; after the first release it becomes a
|
|
25
|
+
# normal publisher. See README.
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write # PyPI Trusted Publishing
|
|
28
|
+
contents: read
|
|
29
|
+
packages: write # the image, to this repo's own container registry
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
publish:
|
|
33
|
+
# ONLY ON A TAG PUSH. A manual run exists to backfill an IMAGE (see `on:` above), and PyPI
|
|
34
|
+
# refuses a version it already holds — so re-uploading is not merely redundant, it fails, and
|
|
35
|
+
# would take the image job down with it.
|
|
36
|
+
if: github.event_name == 'push'
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
environment: pypi
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: astral-sh/setup-uv@v5
|
|
42
|
+
|
|
43
|
+
- name: build
|
|
44
|
+
# No fetch step and no external token: the contract is committed here. A release depends
|
|
45
|
+
# on nothing but this checkout and PyPI.
|
|
46
|
+
run: uv build
|
|
47
|
+
|
|
48
|
+
- name: the wheel must carry its contract
|
|
49
|
+
run: |
|
|
50
|
+
uv venv /tmp/probe
|
|
51
|
+
uv pip install --python /tmp/probe/bin/python -q dist/*.whl
|
|
52
|
+
/tmp/probe/bin/foundry-testing-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-testing
|
|
53
|
+
|
|
54
|
+
- name: the wheel must carry the actor's own cards
|
|
55
|
+
# Same reason as the line above, for the other half of what this package ships. The
|
|
56
|
+
# `-actor` suffix asserts a papeete-actor underneath (ADR-ECO-0022); a wheel published
|
|
57
|
+
# without its cards makes the name a claim the artifact cannot honour, and a published
|
|
58
|
+
# version is not something to discover that from.
|
|
59
|
+
run: |
|
|
60
|
+
/tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
|
|
61
|
+
"$(/tmp/probe/bin/python -c 'from foundry_testing_actor import cards_path; print(cards_path())')"
|
|
62
|
+
|
|
63
|
+
- name: the wheel must carry the default runner
|
|
64
|
+
# A component with no `runner:` is built from the Dockerfile the wheel ships; a published
|
|
65
|
+
# wheel without it fails at a use's first publish, after its commit and push have landed.
|
|
66
|
+
run: |
|
|
67
|
+
/tmp/probe/bin/python -c 'from foundry_testing_actor import runner_path; p = runner_path() / "Dockerfile"; assert p.is_file(), p; print(p)'
|
|
68
|
+
|
|
69
|
+
- name: grounding renders a CLAUDE.md whose imports resolve
|
|
70
|
+
# BEFORE the upload, not after. A wheel that ships without its schema, or whose grounding
|
|
71
|
+
# emits an @-import to a file it never wrote, produces sessions grounded in nothing that
|
|
72
|
+
# look identical to correct ones. That is not something to discover from a published
|
|
73
|
+
# version.
|
|
74
|
+
run: /tmp/probe/bin/python scripts/probe_grounding.py examples/ACME.PARTS.CAP.SUP.007.WID-testing
|
|
75
|
+
|
|
76
|
+
- name: publish to PyPI
|
|
77
|
+
run: uv publish --trusted-publishing always
|
|
78
|
+
|
|
79
|
+
# ── the image, from the same tag and the same checkout ────────────────────────────────────────
|
|
80
|
+
#
|
|
81
|
+
# TWO ARTIFACTS, ONE RELEASE (ADR-FTA-0001, after ADR-FIA-0005). A use pins an image; an
|
|
82
|
+
# embedder pins a wheel; both are this package at this version. A tag that produced one and not the other leaves a consumer
|
|
83
|
+
# unable to take the release at all — so this job builds the wheel again from the same checkout
|
|
84
|
+
# rather than installing the one just uploaded: the image must not depend on PyPI having already
|
|
85
|
+
# indexed it, and must not be able to pick up a different build of the same version.
|
|
86
|
+
#
|
|
87
|
+
# It runs AFTER the wheel is published, not beside it. A failed upload should not leave an image
|
|
88
|
+
# published for a version that does not exist on PyPI; the reverse — a wheel with the image still
|
|
89
|
+
# to come — is recoverable by re-running this job alone.
|
|
90
|
+
image:
|
|
91
|
+
needs: publish
|
|
92
|
+
# `needs` still orders this after the wheel on a tag push. On a manual backfill `publish` is
|
|
93
|
+
# skipped, and a skipped dependency skips its dependents too unless `always()` says otherwise —
|
|
94
|
+
# so the condition has to name both cases rather than relying on `needs` alone.
|
|
95
|
+
if: always() && (needs.publish.result == 'success' || github.event_name == 'workflow_dispatch')
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/checkout@v4
|
|
99
|
+
with:
|
|
100
|
+
# Empty on a tag push, which is `checkout`'s own default — the ref that triggered the
|
|
101
|
+
# run. On a manual backfill it is the tag being republished, so every step below reads
|
|
102
|
+
# that tag's own tree rather than whatever the default branch has moved on to.
|
|
103
|
+
ref: ${{ inputs.tag }}
|
|
104
|
+
|
|
105
|
+
- uses: astral-sh/setup-uv@v5
|
|
106
|
+
|
|
107
|
+
- name: the version is the tag
|
|
108
|
+
# The tag is what a consumer writes in a FROM line. A tag that disagrees with
|
|
109
|
+
# pyproject.toml would publish an image whose own `version()` contradicts its name, which
|
|
110
|
+
# is exactly the confusion the CI gate above exists to prevent for the wheel. On a backfill
|
|
111
|
+
# this compares the named tag against the `pyproject.toml` checked out FROM that tag, so a
|
|
112
|
+
# typo in the input is refused rather than published under the wrong name.
|
|
113
|
+
id: version
|
|
114
|
+
env:
|
|
115
|
+
TAG: ${{ inputs.tag || github.ref_name }}
|
|
116
|
+
run: |
|
|
117
|
+
tag="${TAG#v}"
|
|
118
|
+
declared="$(uv run --quiet python -c 'import tomllib,pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
|
|
119
|
+
[ "$tag" = "$declared" ] || { echo "::error::tag $TAG does not match pyproject version $declared"; exit 1; }
|
|
120
|
+
echo "version=$tag" >> "$GITHUB_OUTPUT"
|
|
121
|
+
|
|
122
|
+
- name: build
|
|
123
|
+
run: uv build
|
|
124
|
+
|
|
125
|
+
- uses: docker/login-action@v3
|
|
126
|
+
with:
|
|
127
|
+
registry: ghcr.io
|
|
128
|
+
username: ${{ github.actor }}
|
|
129
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
130
|
+
|
|
131
|
+
- name: the image, tagged and latest
|
|
132
|
+
env:
|
|
133
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
134
|
+
# `latest` FOLLOWS THE RELEASE STREAM, NOT A BACKFILL. A manual run exists to give an
|
|
135
|
+
# already-released version an image in a registry it missed; run against an older tag it
|
|
136
|
+
# would otherwise drag `latest` backwards onto it, silently, in whichever registry a
|
|
137
|
+
# consumer happens to pull from. The version tag is the whole point of a backfill —
|
|
138
|
+
# `latest` already names something newer, and stays there.
|
|
139
|
+
MOVE_LATEST: ${{ github.event_name == 'push' }}
|
|
140
|
+
run: |
|
|
141
|
+
image="ghcr.io/${{ github.repository }}"
|
|
142
|
+
docker build -f docker/Dockerfile -t "$image:$VERSION" -t "$image:latest" .
|
|
143
|
+
docker push "$image:$VERSION"
|
|
144
|
+
if [ "$MOVE_LATEST" = "true" ]; then docker push "$image:latest"; fi
|
|
145
|
+
|
|
146
|
+
# ── the product's own registry ────────────────────────────────────────────────────────────
|
|
147
|
+
#
|
|
148
|
+
# TWO REGISTRIES, ONE IMAGE. GHCR is where this package publishes; a product that RUNS uses
|
|
149
|
+
# of it pulls from its own registry, which its cluster already authenticates to and which a
|
|
150
|
+
# `FROM` line inside that cluster's builder can reach without a second credential. The base
|
|
151
|
+
# image has to exist in both, because the two are reached by different things: an embedder
|
|
152
|
+
# writing a Dockerfile reads the README and pins GHCR, while a use built by an in-cluster
|
|
153
|
+
# builder resolves its `FROM` against the registry that builder has a push token for.
|
|
154
|
+
#
|
|
155
|
+
# THE SAME IMAGE, NOT ANOTHER BUILD. `docker tag` re-points the build the step above already
|
|
156
|
+
# pushed, so both registries hold one digest. Building a second time would let them diverge —
|
|
157
|
+
# a base-image refresh between two steps of one job is enough — and nothing downstream would
|
|
158
|
+
# ever notice that `:0.1.0` meant two different things depending on where you pulled it.
|
|
159
|
+
#
|
|
160
|
+
# ONE VARIABLE NAMES THE WHOLE TARGET. `vars.PRODUCT_IMAGE` is the full repository path, e.g.
|
|
161
|
+
# `<name>.azurecr.io/<product>/foundry-testing-actor`; the login server is its first
|
|
162
|
+
# segment. This package is generic across capabilities and names no product of its own — the
|
|
163
|
+
# same rule `tests/test_portability.py` enforces over `src/` — so the product that hosts a
|
|
164
|
+
# registry names itself in a repository variable rather than in this file.
|
|
165
|
+
#
|
|
166
|
+
# UNSET IS A VALID STATE, AND IT SAYS SO. A fork, or this repo before its credentials were
|
|
167
|
+
# added, publishes to GHCR alone; that is a `::notice`, not a failure. Configured-but-broken
|
|
168
|
+
# IS a failure — the push below is not best-effort, because an image the product's cluster
|
|
169
|
+
# cannot pull is the whole reason this block exists.
|
|
170
|
+
- name: is a product registry configured?
|
|
171
|
+
id: product
|
|
172
|
+
env:
|
|
173
|
+
PRODUCT_IMAGE: ${{ vars.PRODUCT_IMAGE }}
|
|
174
|
+
run: |
|
|
175
|
+
if [ -z "$PRODUCT_IMAGE" ]; then
|
|
176
|
+
echo "::notice::PRODUCT_IMAGE is unset — publishing to GHCR only"
|
|
177
|
+
echo "configured=false" >> "$GITHUB_OUTPUT"
|
|
178
|
+
exit 0
|
|
179
|
+
fi
|
|
180
|
+
case "$PRODUCT_IMAGE" in
|
|
181
|
+
*/*/*) : ;;
|
|
182
|
+
*) echo "::error::PRODUCT_IMAGE must be <registry>/<path>/<repository>, got $PRODUCT_IMAGE"; exit 1 ;;
|
|
183
|
+
esac
|
|
184
|
+
echo "server=${PRODUCT_IMAGE%%/*}" >> "$GITHUB_OUTPUT"
|
|
185
|
+
echo "configured=true" >> "$GITHUB_OUTPUT"
|
|
186
|
+
|
|
187
|
+
- uses: docker/login-action@v3
|
|
188
|
+
if: steps.product.outputs.configured == 'true'
|
|
189
|
+
with:
|
|
190
|
+
registry: ${{ steps.product.outputs.server }}
|
|
191
|
+
username: ${{ secrets.ACR_PUSH_USERNAME }}
|
|
192
|
+
password: ${{ secrets.ACR_PUSH_PASSWORD }}
|
|
193
|
+
|
|
194
|
+
- name: the same image, in the product's registry
|
|
195
|
+
if: steps.product.outputs.configured == 'true'
|
|
196
|
+
env:
|
|
197
|
+
PRODUCT_IMAGE: ${{ vars.PRODUCT_IMAGE }}
|
|
198
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
199
|
+
MOVE_LATEST: ${{ github.event_name == 'push' }}
|
|
200
|
+
run: |
|
|
201
|
+
ghcr="ghcr.io/${{ github.repository }}"
|
|
202
|
+
docker tag "$ghcr:$VERSION" "$PRODUCT_IMAGE:$VERSION"
|
|
203
|
+
docker push "$PRODUCT_IMAGE:$VERSION"
|
|
204
|
+
if [ "$MOVE_LATEST" = "true" ]; then
|
|
205
|
+
docker tag "$ghcr:latest" "$PRODUCT_IMAGE:latest"
|
|
206
|
+
docker push "$PRODUCT_IMAGE:latest"
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
- name: a use built on it is still an actor
|
|
210
|
+
# BEFORE the image is something anyone can pin — the same reason the grounding probe runs
|
|
211
|
+
# before the upload above. An image that builds but produces a use that does not conform is
|
|
212
|
+
# not something to discover from a published tag.
|
|
213
|
+
run: |
|
|
214
|
+
docker build --build-arg ACTOR_IMAGE=ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }} \
|
|
215
|
+
-t use:release examples/ACME.PARTS.CAP.SUP.007.WID-testing
|
|
216
|
+
docker run --rm use:release foundry-testing-actor lint /actor
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this
|
|
4
|
+
repository.
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
`foundry-testing-actor` runs a headless Claude Code session that authors black-box tests for one
|
|
9
|
+
capability's increment in that capability's own testing repo, then commits, pushes and publishes a
|
|
10
|
+
runnable test image. Before anything is built it answers `propose-acceptance`, its half of the
|
|
11
|
+
three amigos round. It is the **machinery** — it carries no capability of its own. The capability
|
|
12
|
+
arrives in a sidecar (`actor-agentic-context.yaml`, contract
|
|
13
|
+
`foundry-testing-actor/agentic-context/v1`) that the consuming repo writes.
|
|
14
|
+
|
|
15
|
+
Ported from one capability's hand-written testing actor, with `foundry-implementation-actor`
|
|
16
|
+
(FIA) as the template. See `adr/ADR-FTA-0001-*.md`.
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv run --extra dev pytest -q # full suite (what CI runs)
|
|
22
|
+
uv run --extra dev pytest -q tests/test_config.py # one file
|
|
23
|
+
uv run --extra dev pytest -q tests/test_config.py::test_name # one test
|
|
24
|
+
uv build # sdist/wheel (hatchling)
|
|
25
|
+
uv run foundry-testing-actor lint <folder> # validate a sidecar
|
|
26
|
+
uv run foundry-testing-actor show <folder> --registry r # every derived rendering
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
There is no separate lint/format command configured in this repo.
|
|
30
|
+
|
|
31
|
+
## Architecture
|
|
32
|
+
|
|
33
|
+
Modules under `src/foundry_testing_actor/`:
|
|
34
|
+
|
|
35
|
+
- **`config.py`** — `CapabilityConfig`. Loads the sidecar and derives **every** rendering of the
|
|
36
|
+
capability id from `capability` and `source_repo` (plus `implementation_repo`, derived unless
|
|
37
|
+
declared). Carries `lint()`, `Report`, `cards_path()` and `runner_path()`.
|
|
38
|
+
- **`grounding.py`** — copied from FIA: runs each `ground_in` `fetch:`, writes envelopes inside
|
|
39
|
+
the clone, renders the `CLAUDE.md` that `@`-imports the eager ones.
|
|
40
|
+
- **`engine.py`** — `ClaudeCodeTesterEngine`. One engine, two doors, dispatched on the door id.
|
|
41
|
+
`propose-acceptance`: read-only clones (testing repo + implementation DEFAULT branch), `Read,
|
|
42
|
+
Glob,Grep`, small budget, projected JSON reply. `test-task`: rw testing clone on `test/<id>`,
|
|
43
|
+
implementation clone on `impl/<id>` for image recomputation only, full tool session. Never
|
|
44
|
+
commits.
|
|
45
|
+
- **`handler.py`** — `make_test_task(config)`. Containment, commit, push, publish. Never opens a PR.
|
|
46
|
+
- **`correlation.py`** — the two ids and the step vocabulary; byte-identical to FIA's.
|
|
47
|
+
- **`conformance.py`**, **`instance.py`**, **`serve.py`**, **`cli.py`** — copied from FIA, renamed.
|
|
48
|
+
|
|
49
|
+
Beside them, three folders shipped in the wheel: `schemas/` (the sidecar contract), `cards/` (the
|
|
50
|
+
actor's four cards), and `runner/` (the default test-runner Dockerfile a component without
|
|
51
|
+
`runner:` is built with).
|
|
52
|
+
|
|
53
|
+
## Core invariants that any change must preserve
|
|
54
|
+
|
|
55
|
+
- **The proposal is never derived from the build.** `propose-acceptance` clones the implementation
|
|
56
|
+
repo's default branch and never checks out `impl/<task_id>`; `test-task` checks it out but never
|
|
57
|
+
shows it to the session. `tests/test_engine.py` pins both. This is ADR-FIA-0004's "before or
|
|
58
|
+
after", and the whole value of the round.
|
|
59
|
+
- **A door that cannot write beats a door asked not to.** `PROPOSE_TOOLS` carries no Write, Edit or
|
|
60
|
+
Bash. The propose door registers no handler.
|
|
61
|
+
- **The wire contract is shared with two other packages.** Door ids, message names and fields are
|
|
62
|
+
fixed by the three-amigos contract FIA and the orchestration actor also implement. Do not rename
|
|
63
|
+
them; `tests/test_cards.py` pins the shapes.
|
|
64
|
+
- **A use's cards are rendered, and hand copies drift.** `conformance.check` compares the DERIVED
|
|
65
|
+
wire contract, never prose. CI runs `lint-card` against the built wheel.
|
|
66
|
+
- **No capability literal, no knowledge tool name, ever.** `tests/test_portability.py` greps `src/`
|
|
67
|
+
for both. A knowledge source is a `ground_in` entry, never a change here.
|
|
68
|
+
- **The image refs are three-way contracts.** `<registry>/<capability_path>/<component>/tests:<v>`
|
|
69
|
+
(published) and `<registry>/<capability_path>/<component>:<v>` (recomputed, must match what the
|
|
70
|
+
implementation actor published). `config.test_image_ref` / `config.image_ref` output or nothing.
|
|
71
|
+
- **The write boundary is stated once**, as `components[].tests`. Resolution is longest-prefix.
|
|
72
|
+
- **The propose reply is projected** onto the completion message's fields. The framework leaves a
|
|
73
|
+
single-outcome completion schema open, so a session's stray keys would otherwise reach the
|
|
74
|
+
orchestrating actor dressed as part of a shared contract.
|
|
75
|
+
- **The observability record schema is the contract; step names are not.** Every emitted log line
|
|
76
|
+
is budgeted to 64 KB.
|
|
77
|
+
- **The clone is full, never `--depth 1`** — `papeete_version.compute()` needs tags reachable.
|
|
78
|
+
- **The generated `CLAUDE.md` appends** to one the repo already commits. Never overwrite.
|
|
79
|
+
- **The image is one build in two registries** (FIA's ADR-FIA-0006, copied). Never a second
|
|
80
|
+
`docker build`, never a hardcoded product registry.
|
|
81
|
+
- **Never set `ANTHROPIC_API_KEY` in a container running this.**
|
|
82
|
+
|
|
83
|
+
## Scope discipline
|
|
84
|
+
|
|
85
|
+
Generic across **capabilities**, not across actor *kinds*. The machinery shared with FIA is a
|
|
86
|
+
**copy by decision**, not a shared kit (ADR-FTA-0001): do not extract a common package, and do not
|
|
87
|
+
change FIA from here. No hooks, no base classes, no strategy objects for a hypothetical consumer.
|
|
88
|
+
|
|
89
|
+
Design rationale lives in `adr/`. Add a new ADR (copy `adr/template.md`) for any decision of
|
|
90
|
+
similar weight rather than only writing it into code comments.
|
|
91
|
+
|
|
92
|
+
## Releasing
|
|
93
|
+
|
|
94
|
+
Tag-triggered (`v*`) via `.github/workflows/release.yml`: PyPI through Trusted Publishing, the image
|
|
95
|
+
to GHCR and, when `vars.PRODUCT_IMAGE` names one, to a product registry too. A manual run takes a
|
|
96
|
+
tag as input to backfill an image; it skips PyPI and does not move `latest`. `ci.yml` runs the
|
|
97
|
+
suite, the wheel gates (contract, cards, the gate must fail, grounding probe), and builds + boots
|
|
98
|
+
the example use.
|