foundry-implementation-actor 0.2.1__tar.gz → 0.5.1__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_implementation_actor-0.5.1/.github/workflows/ci.yml +132 -0
- foundry_implementation_actor-0.5.1/.github/workflows/release.yml +210 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/.gitignore +1 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/CLAUDE.md +14 -4
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/PKG-INFO +133 -20
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/README.md +129 -19
- foundry_implementation_actor-0.5.1/adr/ADR-FIA-0002-testing-leaves-this-actors-contract.md +104 -0
- foundry_implementation_actor-0.5.1/adr/ADR-FIA-0003-the-dev-test-agreement-has-no-home-yet.md +117 -0
- foundry_implementation_actor-0.5.1/adr/ADR-FIA-0004-the-three-amigos-round.md +141 -0
- foundry_implementation_actor-0.5.1/adr/ADR-FIA-0005-the-actor-ships-an-image.md +133 -0
- foundry_implementation_actor-0.5.1/adr/ADR-FIA-0006-the-image-is-published-to-two-registries.md +104 -0
- foundry_implementation_actor-0.5.1/adr/README.md +24 -0
- foundry_implementation_actor-0.5.1/docker/Dockerfile +105 -0
- foundry_implementation_actor-0.5.1/examples/ACME.PARTS.CAP.SUP.007.WID-implementation/Dockerfile +46 -0
- foundry_implementation_actor-0.5.1/examples/ACME.PARTS.CAP.SUP.007.WID-implementation/actor-agentic-context.yaml +44 -0
- foundry_implementation_actor-0.5.1/examples/README.md +269 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/pyproject.toml +9 -1
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/__init__.py +13 -2
- {foundry_implementation_actor-0.2.1/tests/fixtures/valid → foundry_implementation_actor-0.5.1/src/foundry_implementation_actor/cards}/actor-data.yaml +26 -0
- foundry_implementation_actor-0.5.1/src/foundry_implementation_actor/cards/actor-message.yaml +32 -0
- foundry_implementation_actor-0.5.1/src/foundry_implementation_actor/cards/actor-synchronous-messaging.yaml +56 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/cli.py +63 -6
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/config.py +18 -7
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/conformance.py +28 -14
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/engine.py +273 -17
- foundry_implementation_actor-0.5.1/src/foundry_implementation_actor/instance.py +129 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/schemas/agentic-context.schema.yaml +16 -9
- foundry_implementation_actor-0.5.1/src/foundry_implementation_actor/serve.py +129 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/tests/conftest.py +2 -4
- foundry_implementation_actor-0.5.1/tests/test_cards.py +96 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/tests/test_config.py +18 -4
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/tests/test_conformance.py +61 -8
- foundry_implementation_actor-0.5.1/tests/test_engine.py +385 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/tests/test_handler.py +1 -2
- foundry_implementation_actor-0.5.1/tests/test_instance.py +136 -0
- foundry_implementation_actor-0.5.1/uv.lock +396 -0
- foundry_implementation_actor-0.2.1/.github/workflows/ci.yml +0 -58
- foundry_implementation_actor-0.2.1/.github/workflows/release.yml +0 -49
- foundry_implementation_actor-0.2.1/adr/README.md +0 -19
- foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/cards/actor-data.yaml +0 -38
- foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/cards/actor-message.yaml +0 -17
- foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/cards/actor-synchronous-messaging.yaml +0 -21
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-agentic-context.yaml +0 -28
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-message.yaml +0 -17
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-synchronous-messaging.yaml +0 -21
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor.yaml +0 -8
- foundry_implementation_actor-0.2.1/tests/test_cards.py +0 -49
- foundry_implementation_actor-0.2.1/tests/test_engine.py +0 -144
- foundry_implementation_actor-0.2.1/uv.lock +0 -191
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/adr/ADR-FIA-0001-the-machinery-leaves-the-capability.md +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/adr/template.md +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/scripts/probe_grounding.py +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/cards/actor.yaml +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/correlation.py +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/grounding.py +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/handler.py +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/tests/fixtures/broken/actor-agentic-context.yaml +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/tests/test_cli.py +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/tests/test_grounding.py +0 -0
- {foundry_implementation_actor-0.2.1 → foundry_implementation_actor-0.5.1}/tests/test_portability.py +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
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-implementation-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
37
|
+
/tmp/probe/bin/foundry-implementation-actor show examples/ACME.PARTS.CAP.SUP.007.WID-implementation \
|
|
38
|
+
--registry reg.example.com | tee /tmp/show.txt
|
|
39
|
+
grep -q 'reg.example.com/acme.parts/sup.007.wid/backend:<version>' /tmp/show.txt
|
|
40
|
+
|
|
41
|
+
- name: the wheel must carry the actor's own cards
|
|
42
|
+
# The `-actor` suffix asserts a papeete-actor underneath, which lint-card can check
|
|
43
|
+
# (ADR-ECO-0022). The suite proves the cards are conformant in a source checkout; this
|
|
44
|
+
# proves they SHIPPED. A card folder left out of the build passes every test in tests/ and
|
|
45
|
+
# makes the name a claim the artifact cannot honour. `papeete-actor-synchronous-messaging`
|
|
46
|
+
# is already a runtime dependency, so its gate is in the probe venv with nothing to add.
|
|
47
|
+
run: |
|
|
48
|
+
/tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
|
|
49
|
+
"$(/tmp/probe/bin/python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
|
|
50
|
+
|
|
51
|
+
- name: the gate must run
|
|
52
|
+
# A gate that cannot fail is not a gate. Lint a deliberately non-conformant sidecar with
|
|
53
|
+
# the INSTALLED wheel and require a non-zero exit.
|
|
54
|
+
run: |
|
|
55
|
+
if /tmp/probe/bin/foundry-implementation-actor lint tests/fixtures/broken; then
|
|
56
|
+
echo "::error::lint accepted a non-conformant sidecar — the gate is not running"
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
echo "the gate refused a non-conformant sidecar, as it should"
|
|
60
|
+
|
|
61
|
+
- name: grounding renders a CLAUDE.md whose imports resolve
|
|
62
|
+
# The property the whole design rests on: an @-import naming a file that is not there is a
|
|
63
|
+
# session grounded in nothing, and it looks identical to one grounded correctly.
|
|
64
|
+
run: /tmp/probe/bin/python scripts/probe_grounding.py examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
65
|
+
|
|
66
|
+
# ── the other half of what this package ships ─────────────────────────────────────────────────
|
|
67
|
+
#
|
|
68
|
+
# ADR-FIA-0005: a use is a sidecar, and the image is what turns one into a running actor. The
|
|
69
|
+
# job above proves the WHEEL works. This one proves the claim a consumer actually depends on —
|
|
70
|
+
# that a folder containing one YAML file builds into an actor that boots and answers its doors.
|
|
71
|
+
# Nothing here reaches a registry: the image is built, run and thrown away in the runner.
|
|
72
|
+
image:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v4
|
|
76
|
+
- uses: astral-sh/setup-uv@v5
|
|
77
|
+
|
|
78
|
+
- name: build the wheel this image will carry
|
|
79
|
+
# The image installs the wheel from dist/ rather than from PyPI, so that an image tagged
|
|
80
|
+
# <version> cannot contain some other build of <version> — see docker/Dockerfile.
|
|
81
|
+
run: uv build
|
|
82
|
+
|
|
83
|
+
- name: the base image
|
|
84
|
+
run: docker build -f docker/Dockerfile -t foundry-implementation-actor:ci .
|
|
85
|
+
|
|
86
|
+
- name: the image and the wheel are one release
|
|
87
|
+
# Two artifacts, one version. An image whose wheel says something else is the failure this
|
|
88
|
+
# arrangement creates, so it is the one CI asserts.
|
|
89
|
+
run: |
|
|
90
|
+
wheel="$(ls dist/*.whl | sed -E 's/.*-([0-9][^-]*)-py3.*/\1/')"
|
|
91
|
+
in_image="$(docker run --rm --entrypoint python foundry-implementation-actor:ci \
|
|
92
|
+
-c 'from foundry_implementation_actor import version; print(version())')"
|
|
93
|
+
echo "wheel=$wheel image=$in_image"
|
|
94
|
+
[ "$wheel" = "$in_image" ] || { echo "::error::image carries $in_image, wheel is $wheel"; exit 1; }
|
|
95
|
+
|
|
96
|
+
- name: a use is a sidecar
|
|
97
|
+
# The example folder holds exactly one file besides its Dockerfile. If this builds, the
|
|
98
|
+
# claim in the README is true of a real folder rather than of prose.
|
|
99
|
+
run: |
|
|
100
|
+
test "$(ls examples/ACME.PARTS.CAP.SUP.007.WID-implementation | grep -cv '^Dockerfile$')" = "1"
|
|
101
|
+
docker build --build-arg ACTOR_IMAGE=foundry-implementation-actor:ci \
|
|
102
|
+
-t use:ci examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
103
|
+
|
|
104
|
+
- name: the rendered cards are what a caller is validated against
|
|
105
|
+
# Rendering is the construction that replaces the hand copy conformance.check was built to
|
|
106
|
+
# police. Assert it inside the image, against the cards the image actually runs.
|
|
107
|
+
run: |
|
|
108
|
+
docker run --rm use:ci foundry-implementation-actor lint /actor
|
|
109
|
+
|
|
110
|
+
- name: it boots, and answers the doors its cards declare
|
|
111
|
+
# The credentials are deliberately junk. The engine refuses to construct without a
|
|
112
|
+
# GITHUB_TOKEN — which is itself worth having, and is why they are set rather than omitted
|
|
113
|
+
# — and no door is actually exercised here: a 400 means routed and refused the payload, a
|
|
114
|
+
# 404 would mean the route was never built.
|
|
115
|
+
run: |
|
|
116
|
+
docker run -d --name smoke -p 18080:8080 \
|
|
117
|
+
-e GITHUB_TOKEN=ci -e CLAUDE_CODE_OAUTH_TOKEN=ci use:ci
|
|
118
|
+
for _ in $(seq 1 30); do
|
|
119
|
+
curl -fsS -m 2 http://localhost:18080/health >/dev/null 2>&1 && break
|
|
120
|
+
sleep 1
|
|
121
|
+
done
|
|
122
|
+
curl -fsS -m 5 http://localhost:18080/health | grep -q ok
|
|
123
|
+
for door in implement-task assess-task; do
|
|
124
|
+
code="$(curl -sS -o /dev/null -w '%{http_code}' -m 5 -X POST \
|
|
125
|
+
-H 'content-type: application/json' -d '{}' "http://localhost:18080/$door")"
|
|
126
|
+
[ "$code" = "400" ] || { echo "::error::POST /$door answered $code, expected 400 (routed, bad payload)"; exit 1; }
|
|
127
|
+
done
|
|
128
|
+
code="$(curl -sS -o /dev/null -w '%{http_code}' -m 5 -X POST \
|
|
129
|
+
-H 'content-type: application/json' -d '{}' http://localhost:18080/no-such-door)"
|
|
130
|
+
[ "$code" = "404" ] || { echo "::error::an undeclared door answered $code, expected 404"; exit 1; }
|
|
131
|
+
docker logs smoke | grep -q actor-started
|
|
132
|
+
docker rm -f smoke
|
|
@@ -0,0 +1,210 @@
|
|
|
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.5.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-implementation-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
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_implementation_actor import cards_path; print(cards_path())')"
|
|
62
|
+
|
|
63
|
+
- name: grounding renders a CLAUDE.md whose imports resolve
|
|
64
|
+
# BEFORE the upload, not after. A wheel that ships without its schema, or whose grounding
|
|
65
|
+
# emits an @-import to a file it never wrote, produces sessions grounded in nothing that
|
|
66
|
+
# look identical to correct ones. That is not something to discover from a published
|
|
67
|
+
# version.
|
|
68
|
+
run: /tmp/probe/bin/python scripts/probe_grounding.py examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
69
|
+
|
|
70
|
+
- name: publish to PyPI
|
|
71
|
+
run: uv publish --trusted-publishing always
|
|
72
|
+
|
|
73
|
+
# ── the image, from the same tag and the same checkout ────────────────────────────────────────
|
|
74
|
+
#
|
|
75
|
+
# TWO ARTIFACTS, ONE RELEASE (ADR-FIA-0005). A use pins an image; an embedder pins a wheel; both
|
|
76
|
+
# are this package at this version. A tag that produced one and not the other leaves a consumer
|
|
77
|
+
# unable to take the release at all — so this job builds the wheel again from the same checkout
|
|
78
|
+
# rather than installing the one just uploaded: the image must not depend on PyPI having already
|
|
79
|
+
# indexed it, and must not be able to pick up a different build of the same version.
|
|
80
|
+
#
|
|
81
|
+
# It runs AFTER the wheel is published, not beside it. A failed upload should not leave an image
|
|
82
|
+
# published for a version that does not exist on PyPI; the reverse — a wheel with the image still
|
|
83
|
+
# to come — is recoverable by re-running this job alone.
|
|
84
|
+
image:
|
|
85
|
+
needs: publish
|
|
86
|
+
# `needs` still orders this after the wheel on a tag push. On a manual backfill `publish` is
|
|
87
|
+
# skipped, and a skipped dependency skips its dependents too unless `always()` says otherwise —
|
|
88
|
+
# so the condition has to name both cases rather than relying on `needs` alone.
|
|
89
|
+
if: always() && (needs.publish.result == 'success' || github.event_name == 'workflow_dispatch')
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/checkout@v4
|
|
93
|
+
with:
|
|
94
|
+
# Empty on a tag push, which is `checkout`'s own default — the ref that triggered the
|
|
95
|
+
# run. On a manual backfill it is the tag being republished, so every step below reads
|
|
96
|
+
# that tag's own tree rather than whatever the default branch has moved on to.
|
|
97
|
+
ref: ${{ inputs.tag }}
|
|
98
|
+
|
|
99
|
+
- uses: astral-sh/setup-uv@v5
|
|
100
|
+
|
|
101
|
+
- name: the version is the tag
|
|
102
|
+
# The tag is what a consumer writes in a FROM line. A tag that disagrees with
|
|
103
|
+
# pyproject.toml would publish an image whose own `version()` contradicts its name, which
|
|
104
|
+
# is exactly the confusion the CI gate above exists to prevent for the wheel. On a backfill
|
|
105
|
+
# this compares the named tag against the `pyproject.toml` checked out FROM that tag, so a
|
|
106
|
+
# typo in the input is refused rather than published under the wrong name.
|
|
107
|
+
id: version
|
|
108
|
+
env:
|
|
109
|
+
TAG: ${{ inputs.tag || github.ref_name }}
|
|
110
|
+
run: |
|
|
111
|
+
tag="${TAG#v}"
|
|
112
|
+
declared="$(uv run --quiet python -c 'import tomllib,pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
|
|
113
|
+
[ "$tag" = "$declared" ] || { echo "::error::tag $TAG does not match pyproject version $declared"; exit 1; }
|
|
114
|
+
echo "version=$tag" >> "$GITHUB_OUTPUT"
|
|
115
|
+
|
|
116
|
+
- name: build
|
|
117
|
+
run: uv build
|
|
118
|
+
|
|
119
|
+
- uses: docker/login-action@v3
|
|
120
|
+
with:
|
|
121
|
+
registry: ghcr.io
|
|
122
|
+
username: ${{ github.actor }}
|
|
123
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
124
|
+
|
|
125
|
+
- name: the image, tagged and latest
|
|
126
|
+
env:
|
|
127
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
128
|
+
# `latest` FOLLOWS THE RELEASE STREAM, NOT A BACKFILL. A manual run exists to give an
|
|
129
|
+
# already-released version an image in a registry it missed; run against an older tag it
|
|
130
|
+
# would otherwise drag `latest` backwards onto it, silently, in whichever registry a
|
|
131
|
+
# consumer happens to pull from. The version tag is the whole point of a backfill —
|
|
132
|
+
# `latest` already names something newer, and stays there.
|
|
133
|
+
MOVE_LATEST: ${{ github.event_name == 'push' }}
|
|
134
|
+
run: |
|
|
135
|
+
image="ghcr.io/${{ github.repository }}"
|
|
136
|
+
docker build -f docker/Dockerfile -t "$image:$VERSION" -t "$image:latest" .
|
|
137
|
+
docker push "$image:$VERSION"
|
|
138
|
+
if [ "$MOVE_LATEST" = "true" ]; then docker push "$image:latest"; fi
|
|
139
|
+
|
|
140
|
+
# ── the product's own registry ────────────────────────────────────────────────────────────
|
|
141
|
+
#
|
|
142
|
+
# TWO REGISTRIES, ONE IMAGE. GHCR is where this package publishes; a product that RUNS uses
|
|
143
|
+
# of it pulls from its own registry, which its cluster already authenticates to and which a
|
|
144
|
+
# `FROM` line inside that cluster's builder can reach without a second credential. The base
|
|
145
|
+
# image has to exist in both, because the two are reached by different things: an embedder
|
|
146
|
+
# writing a Dockerfile reads the README and pins GHCR, while a use built by an in-cluster
|
|
147
|
+
# builder resolves its `FROM` against the registry that builder has a push token for.
|
|
148
|
+
#
|
|
149
|
+
# THE SAME IMAGE, NOT ANOTHER BUILD. `docker tag` re-points the build the step above already
|
|
150
|
+
# pushed, so both registries hold one digest. Building a second time would let them diverge —
|
|
151
|
+
# a base-image refresh between two steps of one job is enough — and nothing downstream would
|
|
152
|
+
# ever notice that `:0.5.0` meant two different things depending on where you pulled it.
|
|
153
|
+
#
|
|
154
|
+
# ONE VARIABLE NAMES THE WHOLE TARGET. `vars.PRODUCT_IMAGE` is the full repository path, e.g.
|
|
155
|
+
# `<name>.azurecr.io/<product>/foundry-implementation-actor`; the login server is its first
|
|
156
|
+
# segment. This package is generic across capabilities and names no product of its own — the
|
|
157
|
+
# same rule `tests/test_portability.py` enforces over `src/` — so the product that hosts a
|
|
158
|
+
# registry names itself in a repository variable rather than in this file.
|
|
159
|
+
#
|
|
160
|
+
# UNSET IS A VALID STATE, AND IT SAYS SO. A fork, or this repo before its credentials were
|
|
161
|
+
# added, publishes to GHCR alone; that is a `::notice`, not a failure. Configured-but-broken
|
|
162
|
+
# IS a failure — the push below is not best-effort, because an image the product's cluster
|
|
163
|
+
# cannot pull is the whole reason this block exists.
|
|
164
|
+
- name: is a product registry configured?
|
|
165
|
+
id: product
|
|
166
|
+
env:
|
|
167
|
+
PRODUCT_IMAGE: ${{ vars.PRODUCT_IMAGE }}
|
|
168
|
+
run: |
|
|
169
|
+
if [ -z "$PRODUCT_IMAGE" ]; then
|
|
170
|
+
echo "::notice::PRODUCT_IMAGE is unset — publishing to GHCR only"
|
|
171
|
+
echo "configured=false" >> "$GITHUB_OUTPUT"
|
|
172
|
+
exit 0
|
|
173
|
+
fi
|
|
174
|
+
case "$PRODUCT_IMAGE" in
|
|
175
|
+
*/*/*) : ;;
|
|
176
|
+
*) echo "::error::PRODUCT_IMAGE must be <registry>/<path>/<repository>, got $PRODUCT_IMAGE"; exit 1 ;;
|
|
177
|
+
esac
|
|
178
|
+
echo "server=${PRODUCT_IMAGE%%/*}" >> "$GITHUB_OUTPUT"
|
|
179
|
+
echo "configured=true" >> "$GITHUB_OUTPUT"
|
|
180
|
+
|
|
181
|
+
- uses: docker/login-action@v3
|
|
182
|
+
if: steps.product.outputs.configured == 'true'
|
|
183
|
+
with:
|
|
184
|
+
registry: ${{ steps.product.outputs.server }}
|
|
185
|
+
username: ${{ secrets.ACR_PUSH_USERNAME }}
|
|
186
|
+
password: ${{ secrets.ACR_PUSH_PASSWORD }}
|
|
187
|
+
|
|
188
|
+
- name: the same image, in the product's registry
|
|
189
|
+
if: steps.product.outputs.configured == 'true'
|
|
190
|
+
env:
|
|
191
|
+
PRODUCT_IMAGE: ${{ vars.PRODUCT_IMAGE }}
|
|
192
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
193
|
+
MOVE_LATEST: ${{ github.event_name == 'push' }}
|
|
194
|
+
run: |
|
|
195
|
+
ghcr="ghcr.io/${{ github.repository }}"
|
|
196
|
+
docker tag "$ghcr:$VERSION" "$PRODUCT_IMAGE:$VERSION"
|
|
197
|
+
docker push "$PRODUCT_IMAGE:$VERSION"
|
|
198
|
+
if [ "$MOVE_LATEST" = "true" ]; then
|
|
199
|
+
docker tag "$ghcr:latest" "$PRODUCT_IMAGE:latest"
|
|
200
|
+
docker push "$PRODUCT_IMAGE:latest"
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
- name: a use built on it is still an actor
|
|
204
|
+
# BEFORE the image is something anyone can pin — the same reason the grounding probe runs
|
|
205
|
+
# before the upload above. An image that builds but produces a use that does not conform is
|
|
206
|
+
# not something to discover from a published tag.
|
|
207
|
+
run: |
|
|
208
|
+
docker build --build-arg ACTOR_IMAGE=ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }} \
|
|
209
|
+
-t use:release examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
210
|
+
docker run --rm use:release foundry-implementation-actor lint /actor
|
|
@@ -88,6 +88,11 @@ Beside them, two folders of committed contract, both shipped in the wheel:
|
|
|
88
88
|
- **The clone is full, never `--depth 1`.** `papeete_version.compute()` runs `git describe --tags`
|
|
89
89
|
against it and needs the matching tag's commit reachable.
|
|
90
90
|
- **The generated `CLAUDE.md` appends** to one the repo already commits. Never overwrite.
|
|
91
|
+
- **The image is one build in two registries.** GHCR is where this package publishes; a product's
|
|
92
|
+
own registry is where the in-cluster builder that resolves a use's `FROM` line can actually
|
|
93
|
+
authenticate (ADR-FIA-0006). `release.yml` `docker tag`s one build into both so they hold one
|
|
94
|
+
digest — never add a second `docker build`, and never hardcode a registry: the product names
|
|
95
|
+
itself in `vars.PRODUCT_IMAGE`, the same reason `src/` names no capability.
|
|
91
96
|
- **Never set `ANTHROPIC_API_KEY` in a container running this.** In `claude -p` non-interactive
|
|
92
97
|
mode an API key in the environment is always preferred over `CLAUDE_CODE_OAUTH_TOKEN`, silently
|
|
93
98
|
routing every session through metered billing. There is no warning; the only symptom is the bill.
|
|
@@ -104,7 +109,12 @@ similar weight rather than only writing it into code comments.
|
|
|
104
109
|
## Releasing
|
|
105
110
|
|
|
106
111
|
Tag-triggered (`v*`) via `.github/workflows/release.yml`, publishing to PyPI through Trusted
|
|
107
|
-
Publishing (OIDC) — no stored token. The
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
Publishing (OIDC) — no stored token. The image goes to GHCR and, when `vars.PRODUCT_IMAGE` names
|
|
113
|
+
one, to a product's own registry as well (ADR-FIA-0006). The workflow also accepts a manual run
|
|
114
|
+
that takes a tag as an INPUT — dispatched from the default branch, because the file comes from the
|
|
115
|
+
dispatched ref and a tag needing a backfill predates the workflow that can do it — to give an
|
|
116
|
+
already-released version an image in a registry it missed; that run skips PyPI and does not move
|
|
117
|
+
`latest`. The release job builds the wheel, installs it into
|
|
118
|
+
a throwaway venv, and renders a `CLAUDE.md` from a fixture sidecar, asserting its `@`-imports
|
|
119
|
+
resolve, before publishing. `ci.yml` runs the suite plus two gates — *the wheel must carry its
|
|
120
|
+
contract* and *the gate must run* — and reaches nothing outside its own checkout.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: foundry-implementation-actor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Runs a headless Claude Code implementation session against one capability's own repo — a papeete-actor for one use, with the capability supplied by a sidecar.
|
|
5
5
|
Project-URL: Homepage, https://github.com/papeete-hub/foundry-implementation-actor
|
|
6
6
|
Author-email: Papeete Consulting <yoann.remy@outlook.com>
|
|
@@ -16,6 +16,9 @@ Requires-Dist: papeete-version>=0.1.0
|
|
|
16
16
|
Requires-Dist: pyyaml>=6.0
|
|
17
17
|
Provides-Extra: dev
|
|
18
18
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
19
|
+
Provides-Extra: serve
|
|
20
|
+
Requires-Dist: papeete-actor-synchronous-messaging-http>=0.4.0; extra == 'serve'
|
|
21
|
+
Requires-Dist: papeete-observability>=0.1.0; extra == 'serve'
|
|
19
22
|
Description-Content-Type: text/markdown
|
|
20
23
|
|
|
21
24
|
# foundry-implementation-actor
|
|
@@ -32,6 +35,10 @@ features *of* the framework (`ADR-ECO-0022`).
|
|
|
32
35
|
pip install foundry-implementation-actor
|
|
33
36
|
```
|
|
34
37
|
|
|
38
|
+
> **New here?** [`examples/`](examples/) walks through a complete, working use of this actor —
|
|
39
|
+
> what the workflow is, what it is for, and how to instantiate one for your own capability. Every
|
|
40
|
+
> command in it runs with no credentials and no network.
|
|
41
|
+
|
|
35
42
|
## What it is
|
|
36
43
|
|
|
37
44
|
The actor's **definition** — its four cards, and the machinery behind them. It carries **no
|
|
@@ -45,8 +52,8 @@ capability: ACME.PARTS.CAP.SUP.007.WID
|
|
|
45
52
|
source_repo: acme-lab/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
46
53
|
registry_repo: acme-lab/acme-governance
|
|
47
54
|
components:
|
|
48
|
-
- {name: backend, path: backend/,
|
|
49
|
-
- {name: stub, path: stub/,
|
|
55
|
+
- {name: backend, path: backend/, dockerfile: backend/deployment/local}
|
|
56
|
+
- {name: stub, path: stub/, dockerfile: stub/deployment/local}
|
|
50
57
|
ground_in:
|
|
51
58
|
- name: business
|
|
52
59
|
answers: the WHAT/WHY — domain vision, business events, ubiquitous language
|
|
@@ -60,7 +67,27 @@ ground_in:
|
|
|
60
67
|
load: on-demand
|
|
61
68
|
```
|
|
62
69
|
|
|
63
|
-
and
|
|
70
|
+
and, beside it, a four-line Dockerfile:
|
|
71
|
+
|
|
72
|
+
```dockerfile
|
|
73
|
+
FROM ghcr.io/papeete-hub/foundry-implementation-actor:0.5.1
|
|
74
|
+
RUN pip install --no-cache-dir kpack==2.0.1 kontract==0.1.0 # what this sidecar's ground_in names
|
|
75
|
+
COPY actor-agentic-context.yaml /actor/
|
|
76
|
+
RUN foundry-implementation-actor render-cards /actor && foundry-implementation-actor lint /actor
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**That is the whole repository.** No cards, no entrypoint, no Python. The cards are rendered from
|
|
80
|
+
the definition in the image, the entrypoint is `foundry-implementation-actor serve`, and the only
|
|
81
|
+
hand-written line naming anything is the `pip install` of the knowledge tools this capability's own
|
|
82
|
+
`ground_in:` argv happens to name — which is the consumer's fact by design (ADR-FIA-0005).
|
|
83
|
+
|
|
84
|
+
A second capability instantiates the same actor by writing that sidecar. Nothing here is
|
|
85
|
+
subclassed, hooked, or configured with a strategy object — there is one shape, and it is this one.
|
|
86
|
+
|
|
87
|
+
### Embedding it instead
|
|
88
|
+
|
|
89
|
+
A consumer that wants its own base image, or the actor inside a larger process, skips all of the
|
|
90
|
+
above and wires it in four lines:
|
|
64
91
|
|
|
65
92
|
```python
|
|
66
93
|
from foundry_implementation_actor import CapabilityConfig, ClaudeCodeEngine, make_implement_task
|
|
@@ -71,8 +98,10 @@ actor = Actor.from_card(".", mailbox=mailbox,
|
|
|
71
98
|
actions={"implement-task": make_implement_task(config)})
|
|
72
99
|
```
|
|
73
100
|
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
`assess-task` needs no entry here: it is a query with an engine and no handler, so the engine's own
|
|
102
|
+
judgement is the reply. `pip install foundry-implementation-actor` is enough for this; the mailbox
|
|
103
|
+
and the observability backend that `serve` needs live in the `[serve]` extra, so an embedder is not
|
|
104
|
+
handed an HTTP server it did not ask for.
|
|
76
105
|
|
|
77
106
|
## The definition, and a use
|
|
78
107
|
|
|
@@ -81,11 +110,14 @@ four cards — who it is, the data it knows, the messages it exchanges, and the
|
|
|
81
110
|
door it answers — and they ship in the wheel, reachable as `cards_path()`. They name no capability,
|
|
82
111
|
because which capability an instance serves is not part of what the actor *is*.
|
|
83
112
|
|
|
84
|
-
A **use** of this actor is one capability's own folder:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
113
|
+
A **use** of this actor is one capability's own folder: the `actor-agentic-context.yaml` that binds
|
|
114
|
+
it to that capability's repository and knowledge base, and the four cards named for the capability
|
|
115
|
+
it serves — **rendered from the definition, not copied.** `foundry-implementation-actor
|
|
116
|
+
render-cards` writes them, the image runs it at `docker build` time, and a use's repository
|
|
117
|
+
therefore holds one hand-written YAML file. That is why the cards live in the wheel rather than in
|
|
118
|
+
an `examples/` folder: they are build input, not documentation (ADR-FIA-0005).
|
|
119
|
+
|
|
120
|
+
They used to be copied by hand, which is why the gate below exists.
|
|
89
121
|
|
|
90
122
|
The split is what the name asserts. `ADR-ECO-0022` makes the `-actor` suffix an obligation: a
|
|
91
123
|
package ending in `-actor` claims a `papeete-actor` underneath, *"and a `<use>-<tier>-actor` that
|
|
@@ -97,12 +129,17 @@ papeete-actor-synchronous-messaging lint-card \
|
|
|
97
129
|
"$(python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
|
|
98
130
|
```
|
|
99
131
|
|
|
100
|
-
A
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
A hand copy drifts: both folders pass `lint-card` independently, and neither gate has an opinion
|
|
133
|
+
about the other. `foundry-implementation-actor lint` therefore runs a second check —
|
|
134
|
+
`conformance.check` — comparing the use's cards against the definition's on the **derived wire
|
|
135
|
+
contract**: the set of doors, and each door's `request_schema`, `completion_schema` and `engine`.
|
|
136
|
+
Those derivations already fold in the data dictionary and the message catalog, so a renamed item or
|
|
137
|
+
a changed reference lands in the payload a caller is validated against.
|
|
138
|
+
|
|
139
|
+
Rendering makes that gate cheap to pass rather than redundant, and both are worth having: a use
|
|
140
|
+
still carrying a hand copy from before 0.5.0, or one pinned to an older image, is exactly the case
|
|
141
|
+
that can be wrong — and a construction that cannot drift is still better than a check that catches
|
|
142
|
+
drift, which is why the rendering exists at all.
|
|
106
143
|
|
|
107
144
|
Prose is not compared, on purpose: a use *should* name its real capability and its real peers, and
|
|
108
145
|
`actor.yaml`'s `name:` is its own identity and is required to differ.
|
|
@@ -111,7 +148,16 @@ Because the cards sit under `src/`, `tests/test_portability.py` greps them too
|
|
|
111
148
|
a knowledge tool name written into the actor's own definition fails the build exactly as it would
|
|
112
149
|
in the code.
|
|
113
150
|
|
|
114
|
-
##
|
|
151
|
+
## Two doors
|
|
152
|
+
|
|
153
|
+
| door | verb | what it does |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `implement-task` | request | builds the increment, commits, pushes a branch, publishes one image per touched component |
|
|
156
|
+
| `assess-task` | query | answers whether a proposed acceptance surface can be delivered. Writes nothing |
|
|
157
|
+
|
|
158
|
+
Both name the same engine. `Actor.judge()` hands it the door id, and it dispatches on that.
|
|
159
|
+
|
|
160
|
+
### `implement-task`
|
|
115
161
|
|
|
116
162
|
```
|
|
117
163
|
clone (full, not --depth 1)
|
|
@@ -126,6 +172,44 @@ clone (full, not --depth 1)
|
|
|
126
172
|
It never opens a pull request. Rendering a verdict and opening one belongs to whichever actor
|
|
127
173
|
orchestrates the pipeline, once its other members have also confirmed.
|
|
128
174
|
|
|
175
|
+
## The three amigos round
|
|
176
|
+
|
|
177
|
+
Before any of that, the actor that will black-box test the increment says what it intends to
|
|
178
|
+
assert, and this one answers whether that can be delivered:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
orchestration ──▶ testing: propose the acceptance surface for this task
|
|
182
|
+
◀── expectations[] — each with a stable id, a statement, a handle
|
|
183
|
+
orchestration ──▶ implementation: assess-task — can you deliver this?
|
|
184
|
+
◀── feasible + objections[] + commitments[]
|
|
185
|
+
|
|
186
|
+
agreed → the surface goes to implement-task AND test-task, on every attempt
|
|
187
|
+
disagreed → orchestration stops and hands the objections back to a human
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Exactly one round, then a person.** No counter-proposal and no negotiation loop: a three-amigos
|
|
191
|
+
meeting converges because a human is in the room, and here the human *is* the tie-breaker. *"The
|
|
192
|
+
task does not determine this"* is a legitimate answer, and it is the most useful one.
|
|
193
|
+
|
|
194
|
+
`assess-task` is a **query**, not an action — *"a promise to answer, from this actor's own state
|
|
195
|
+
and nothing invented."* It clones read-only, grounds itself exactly as the implement door does, and
|
|
196
|
+
is invoked with `--tools Read,Glob,Grep`, so `Write`, `Edit` and `Bash` are absent from the session
|
|
197
|
+
rather than merely unapproved (0.5.0 passed only `--allowedTools`, which removes nothing — see
|
|
198
|
+
ADR-FIA-0004's amendment). A door that **cannot** write
|
|
199
|
+
beats a door asked not to. It registers no handler either: with an engine and no handler,
|
|
200
|
+
`Actor.receive()` returns the judged dict as the reply, and there is nothing to contain because
|
|
201
|
+
nothing is produced.
|
|
202
|
+
|
|
203
|
+
What it commits to is a **promise, not a report** — nothing has been built when it answers. That
|
|
204
|
+
distinction is the whole point (`ADR-FIA-0004`): a tester deriving its assertions from what was
|
|
205
|
+
already built can only ever confirm the build. The failure this prevents is real and already in the
|
|
206
|
+
wild — an e2e suite carrying three fixture ids its own comment admits were *"discovered black-box
|
|
207
|
+
against the running container"*, against a task card that never named them.
|
|
208
|
+
|
|
209
|
+
`acceptance_surface` then rides along as an **optional** field on `implement-task`. Without it the
|
|
210
|
+
door behaves exactly as before; with it, where it is more specific than the definition of done, it
|
|
211
|
+
wins.
|
|
212
|
+
|
|
129
213
|
## Grounding is a precondition, not a request
|
|
130
214
|
|
|
131
215
|
`CLAUDE.md` at the working directory's root is loaded by the `claude` CLI **before the first
|
|
@@ -206,11 +290,14 @@ this can be an ordinary Pod.
|
|
|
206
290
|
## CLI
|
|
207
291
|
|
|
208
292
|
```bash
|
|
209
|
-
foundry-implementation-actor lint . # validate the sidecar
|
|
293
|
+
foundry-implementation-actor lint . # validate the sidecar and the cards
|
|
210
294
|
foundry-implementation-actor show . --registry reg.example.com # every derived rendering
|
|
295
|
+
foundry-implementation-actor render-cards . # write the four cards from the wheel
|
|
296
|
+
foundry-implementation-actor serve . # boot it (needs the `serve` extra)
|
|
211
297
|
```
|
|
212
298
|
|
|
213
|
-
`
|
|
299
|
+
`render-cards` and `serve` are what the image runs, and are usable anywhere the wheel is. `lint` is
|
|
300
|
+
the gate CI runs. A sidecar declaring some other `context:` is read, warned, and not
|
|
214
301
|
checked further — UNMIGRATED is not the same as non-conformant, and migrating is the owning pair's
|
|
215
302
|
own act.
|
|
216
303
|
|
|
@@ -244,6 +331,32 @@ move, not a rewrite."*
|
|
|
244
331
|
|
|
245
332
|
`adr/` records the decisions. Design rationale belongs there, not in commit messages.
|
|
246
333
|
|
|
334
|
+
## Releasing, and which registry to pin
|
|
335
|
+
|
|
336
|
+
A tag (`v*`) publishes two artifacts at one version — the wheel to PyPI, and the image to **two
|
|
337
|
+
registries** holding the same digest (ADR-FIA-0006):
|
|
338
|
+
|
|
339
|
+
| Registry | Pin it when |
|
|
340
|
+
|---|---|
|
|
341
|
+
| `ghcr.io/papeete-hub/foundry-implementation-actor` | you are writing a use's Dockerfile yourself, and build it with your own Docker |
|
|
342
|
+
| a product's own registry, `vars.PRODUCT_IMAGE` | the use is built **in-cluster**, by a builder whose one registry credential is that product's |
|
|
343
|
+
|
|
344
|
+
The second exists because `buildctl` resolves a `FROM` line client-side against the single registry
|
|
345
|
+
credential it was handed — so a use built by the cluster's shared builder can only reach the
|
|
346
|
+
product's own registry, whatever the README says. The push is skipped, with a notice, when
|
|
347
|
+
`vars.PRODUCT_IMAGE` is unset.
|
|
348
|
+
|
|
349
|
+
To give an already-released version an image in a registry it missed, run the workflow by hand from
|
|
350
|
+
the default branch with the tag as its input — it checks that tag out, and refuses it if it
|
|
351
|
+
disagrees with the `pyproject.toml` beside it:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
gh workflow run release.yml --ref main -f tag=v0.5.0
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
That run skips PyPI (which refuses a version it already holds) and does not move `latest` in either
|
|
358
|
+
registry.
|
|
359
|
+
|
|
247
360
|
## Development
|
|
248
361
|
|
|
249
362
|
```bash
|