From fabde0875219cdc8075d86cf2e324c6bc4a949c7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 24 Sep 2026 14:10:02 -0400 Subject: [PATCH] Pin the US publish-now, flip-later flow on the release-id tag (#450) After a --tag-only or --no-latest publish, the standard publish of the same release directory must reuse the release-id tag and write only the latest.json commit. #966's tag reuse in _publish_atomic already covers this path for every pointer, not only line pointers; these tests pin it for the US default and fail with the original 409 ("tag exists") when the existing-tag lookup is disabled. A tag that describes another cut refuses before any commit. README says how to flip. Closes #450. Co-Authored-By: Claude Opus 5.5 --- README.md | 7 +- .../450-us-default-flip-reuses-tag.fixed.md | 1 + packages/microcosm-data/tests/test_release.py | 87 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 changelog.d/450-us-default-flip-reuses-tag.fixed.md diff --git a/README.md b/README.md index 0476e0984..80eb56b0d 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,12 @@ main branch. The launcher also forces `--no-staging`, so the build writes neithe a production nor a staging pointer. The candidate is therefore available only by its explicit release id or tag until a separate promotion updates `latest.json`. Because Slack alerts are coupled to that production pointer -update, tag-only publication sends no release alert. +update, tag-only publication sends no release alert. The promotion is the +standard publish of the same release directory, without `--no-latest` and +`--tag-only`: it reuses the existing release-id tag once the tagged +`release_manifest.json` is byte-identical to the local one, and writes only the +main commit that carries `latest.json` (microcosm#450). A tag that describes +another cut refuses before any commit. Evidence-tier releases (microcosm#506) are the third lane: the best available artifact when terminal gates failed, built with diff --git a/changelog.d/450-us-default-flip-reuses-tag.fixed.md b/changelog.d/450-us-default-flip-reuses-tag.fixed.md new file mode 100644 index 000000000..12d0ff818 --- /dev/null +++ b/changelog.d/450-us-default-flip-reuses-tag.fixed.md @@ -0,0 +1 @@ +Pin the US publish-now, flip-later flow: after a `--tag-only` or `--no-latest` publish, the standard publish of the same release directory reuses the release-id tag and writes only the `latest.json` commit, and refuses a tag that describes another cut. The tag reuse from #966 already covered this path; the new tests fail with the original 409 (`tag exists`) when it is disabled (microcosm#450). diff --git a/packages/microcosm-data/tests/test_release.py b/packages/microcosm-data/tests/test_release.py index fbb54f488..7a727c77b 100644 --- a/packages/microcosm-data/tests/test_release.py +++ b/packages/microcosm-data/tests/test_release.py @@ -1171,6 +1171,93 @@ def test_exact_k_tag_only_publish_never_mutates_main( ) +@pytest.mark.parametrize("tag_only", [True, False], ids=["tag-only", "no-latest"]) +def test_us_default_flip_after_a_non_default_publish_reuses_the_release_tag( + hub: FakeHub, release_dir: Path, artifact_root: Path, tag_only: bool, monkeypatch +) -> None: + """microcosm#450: publish immutable now, make it the default later. + + The first call cuts the release-id tag without touching ``latest.json`` + (``--tag-only`` or ``--no-latest``). The flip is the plain default + publish of the same release directory: it recognises the tag that + already describes this release, writes no second immutable revision + and no second tag, and lands only the main commit carrying the pointer. + Before #966 the flip died on ``create_tag`` with a 409. + """ + monkeypatch.setattr(release_module, "notify_release", lambda *a, **k: None) + publish_release( + release_dir, + "policyengine/populace-us", + api=hub, + artifact_root=artifact_root, + updated_at="2026-06-11T13:53:15+00:00", + update_latest=False, + tag_only=tag_only, + ) + tags_after_cut = list(hub.tags) + assert [tag["tag"] for tag in tags_after_cut] == [RELEASE_ID] + tagged_revision = hub._refs[RELEASE_ID] + assert LATEST_POINTER_PATH not in hub._commits[hub._refs["main"]] + main_before_flip = hub._refs["main"] + events_after_cut = len(hub.events) + + publish_release( + release_dir, + "policyengine/populace-us", + api=hub, + artifact_root=artifact_root, + updated_at="2026-06-12T09:00:00+00:00", + ) + + flip_events = hub.events[events_after_cut:] + assert [event for event, _ in flip_events] == ["create_commit"] + flip = flip_events[0][1] + assert flip["revision"] == "main" + assert flip["parent_commit"] == main_before_flip + assert LATEST_POINTER_PATH in flip["paths"] + assert hub.tags == tags_after_cut + assert hub._refs[RELEASE_ID] == tagged_revision + pointer = latest_release("policyengine/populace-us", api=hub) + assert pointer.release_id == RELEASE_ID + + +def test_us_default_flip_refuses_a_release_tag_that_describes_another_cut( + hub: FakeHub, release_dir: Path, artifact_root: Path, monkeypatch +) -> None: + """microcosm#450: the flip reuses a tag only when it is this release. + + A local release directory whose manifest differs from the tagged one + (a rebuilt cut under the same release id) must refuse before any main + commit, so the default pointer never names bytes nobody published. + """ + monkeypatch.setattr(release_module, "notify_release", lambda *a, **k: None) + publish_release( + release_dir, + "policyengine/populace-us", + api=hub, + artifact_root=artifact_root, + updated_at="2026-06-11T13:53:15+00:00", + update_latest=False, + tag_only=True, + ) + main_before_flip = hub._refs["main"] + events_after_cut = len(hub.events) + manifest_path = release_dir / "release_manifest.json" + manifest_path.write_bytes(manifest_path.read_bytes() + b"\n") + + with pytest.raises(ValueError, match="describes another"): + publish_release( + release_dir, + "policyengine/populace-us", + api=hub, + artifact_root=artifact_root, + updated_at="2026-06-12T09:00:00+00:00", + ) + + assert hub.events[events_after_cut:] == [] + assert hub._refs["main"] == main_before_flip + + @pytest.mark.parametrize( ("publish_options", "message"), [