From f460e33ef6f9f84e6276aa1cc18243f572135c00 Mon Sep 17 00:00:00 2001 From: Arpan Ghoshal Date: Wed, 16 Sep 2026 05:24:21 +0530 Subject: [PATCH 1/2] Let a tag release the LangChain adapter, and catch the next one that cannot #225 merged an adapter no tag could publish. publish.yml's trigger list and its case statement are both explicit and neither had a row, so a tag naming it would have exited 1. Nothing failed, because the tag test transcribes the *version* rule and never asked whether the workflow knows the adapter exists. Three changes, the second of which is the one that matters: ADAPTER_DIRECTORIES was a literal, which is why none of the three parametrised packaging tests covered the new adapter. Derived from the tree now, the rule the repo's own instructions already state for guarantee ids. A new test asserts every adapter in the tree has both a tag trigger and a case row in publish.yml. Mutation-checked: removing either fails it. Deriving the list pointed the two PUBLISHED.toml guards at an adapter with no published side, correctly. Absence is recorded now rather than tolerated, via an `unreleased` list, and an adapter in neither a version table nor that list fails. A row that is simply missing would read as "not published yet", which is this file's guard dying in the one way it has not died yet. Mutation-checked the same way. Signed-off-by: Arpan Ghoshal --- .github/workflows/publish.yml | 2 ++ adapters/PUBLISHED.toml | 6 ++++ tests/test_packaging.py | 55 +++++++++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8abd2c5..8426b3a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,6 +16,7 @@ on: - "v*" - "adapters-langgraph-*" - "adapters-openai-agents-*" + - "adapters-langchain-*" permissions: contents: read @@ -44,6 +45,7 @@ jobs: v*) target=ctrlrun; path=.; kernel=true ;; adapters-langgraph-*) target=ctrlrun-langgraph; path=adapters/langgraph; kernel=false ;; adapters-openai-agents-*) target=ctrlrun-openai-agents; path=adapters/openai-agents; kernel=false ;; + adapters-langchain-*) target=ctrlrun-langchain; path=adapters/langchain; kernel=false ;; *) echo "::error::$GITHUB_REF_NAME names no distribution"; exit 1 ;; esac echo "target=$target" >> "$GITHUB_OUTPUT" diff --git a/adapters/PUBLISHED.toml b/adapters/PUBLISHED.toml index e00bf03..ecd25c3 100644 --- a/adapters/PUBLISHED.toml +++ b/adapters/PUBLISHED.toml @@ -57,6 +57,12 @@ # network, and `SPEC-v0.4 §3.9`'s discipline applies: a check that cannot run offline is a check # that gets skipped in the run that mattered. +# Adapters that exist in the tree and have never been uploaded. An adapter belongs here or in a +# version table below, never in neither: absence alone would let a deleted row read as "not +# published yet", which is this file's guard dying in the one way it has not yet died. A release +# pass moves a name out of here and adds its table **after** the upload succeeds. +unreleased = ["langchain"] + [langgraph] version = "1.3.0" kernel = ">=0.5,<0.13" diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 7781c10..e97710a 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -806,7 +806,42 @@ def test_the_throwaway_sector_configuration_ships_nowhere(): ) -ADAPTER_DIRECTORIES = ("langgraph", "openai-agents") +def _adapter_directories() -> tuple[str, ...]: + """Every adapter in the tree, derived rather than listed. + + It was a literal, and `ctrlrun-langchain` landed without any of the three tests below + noticing: the tag rule, the widened-range guard and the published record all took their + subjects from a tuple nobody updated. A count or a set a test can derive is one a test + derives. The same rule the guarantee ids follow, for the same reason: a hand-maintained + list of subjects is a list that silently stops covering things. + """ + adapters = REPO_ROOT / "adapters" + if not adapters.is_dir(): # pragma: no cover - the sdist prunes adapters/ + return () + return tuple( + sorted(path.name for path in adapters.iterdir() if (path / "pyproject.toml").is_file()) + ) + + +ADAPTER_DIRECTORIES = _adapter_directories() + + +def test_every_adapter_in_the_tree_is_one_publish_yml_can_release(): + """An adapter `publish.yml` does not name is one no tag can ship. + + `ctrlrun-langchain` merged with its directory, its distribution and its tests, and no tag + could have published it: the workflow's trigger list and its `case` statement are both + explicit, and neither had a row. Nothing failed, because the tag test above transcribes the + *version* rule and never asks whether the workflow knows the adapter exists. + """ + workflow = (REPO_ROOT / ".github" / "workflows" / "publish.yml").read_text(encoding="utf-8") + for adapter in ADAPTER_DIRECTORIES: + assert f'"adapters-{adapter}-*"' in workflow, ( + f"adapters/{adapter} has no tag trigger in publish.yml, so no tag can release it" + ) + assert f"adapters-{adapter}-*)" in workflow, ( + f"adapters/{adapter} has no case row in publish.yml, so a tag naming it would exit 1" + ) @pytest.mark.parametrize("adapter", ADAPTER_DIRECTORIES) @@ -878,7 +913,14 @@ def test_a_widened_kernel_range_is_not_shipped_without_a_new_version(adapter): pytest.skip("adapters/ is not in this distribution, which SPEC-v0.5 §6.1 requires") with published_file.open("rb") as handle: - published = _tomllib.load(handle)[adapter] + record = _tomllib.load(handle) + if adapter in record.get("unreleased", []): + pytest.skip(f"adapters/{adapter} is recorded as never uploaded; there is no published side") + assert adapter in record, ( + f"adapters/{adapter} is in neither a version table nor `unreleased` in PUBLISHED.toml. " + "A row that is simply missing reads as 'not published yet', which is how this guard dies." + ) + published = record[adapter] with manifest.open("rb") as handle: project = _tomllib.load(handle)["project"] @@ -946,7 +988,14 @@ def test_the_published_record_is_a_record_and_not_a_plan(adapter): pytest.skip("adapters/ is not in this distribution, which SPEC-v0.5 §6.1 requires") with published_file.open("rb") as handle: - published = _tomllib.load(handle)[adapter] + record = _tomllib.load(handle) + if adapter in record.get("unreleased", []): + pytest.skip(f"adapters/{adapter} is recorded as never uploaded; there is no published side") + assert adapter in record, ( + f"adapters/{adapter} is in neither a version table nor `unreleased` in PUBLISHED.toml. " + "A row that is simply missing reads as 'not published yet', which is how this guard dies." + ) + published = record[adapter] version, kernel = published["version"], published["kernel"] known = RECORDED[adapter] From d6015c4c860fd42067268d8d63bc810774850f82 Mon Sep 17 00:00:00 2001 From: Arpan Ghoshal Date: Wed, 16 Sep 2026 05:39:07 +0530 Subject: [PATCH 2/2] Skip the publish.yml wiring check where publish.yml is not shipped The package job unpacks the sdist and runs the suite from inside it, and the sdist prunes .github/ along with adapters/. The new test read the workflow before looking, so it raised FileNotFoundError there instead of skipping. Same guard the section's other documentation tests already use for adapters/, and the reason is the same: these tests read repository files that are deliberately absent from a distribution, so not finding them is the expected state and not a failure. Verified against a directory holding only tests/, where it skips, and against the tree, where it still fails if a trigger is removed. Signed-off-by: Arpan Ghoshal --- tests/test_packaging.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_packaging.py b/tests/test_packaging.py index e97710a..10d4c1e 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -834,7 +834,10 @@ def test_every_adapter_in_the_tree_is_one_publish_yml_can_release(): explicit, and neither had a row. Nothing failed, because the tag test above transcribes the *version* rule and never asks whether the workflow knows the adapter exists. """ - workflow = (REPO_ROOT / ".github" / "workflows" / "publish.yml").read_text(encoding="utf-8") + source = REPO_ROOT / ".github" / "workflows" / "publish.yml" + if not source.is_file(): # pragma: no cover - the sdist carries neither .github/ nor adapters/ + pytest.skip("publish.yml is not in this distribution; the sdist prunes .github/") + workflow = source.read_text(encoding="utf-8") for adapter in ADAPTER_DIRECTORIES: assert f'"adapters-{adapter}-*"' in workflow, ( f"adapters/{adapter} has no tag trigger in publish.yml, so no tag can release it"