From 849512e0adec756a2877bc6b200855426ed68f62 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 21 Apr 2026 23:10:22 -0400 Subject: [PATCH 1/3] style: ruff format --- tests/unit/actions/test_snap_installer.py | 92 +++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/unit/actions/test_snap_installer.py b/tests/unit/actions/test_snap_installer.py index 71f78ae08..51f211402 100644 --- a/tests/unit/actions/test_snap_installer.py +++ b/tests/unit/actions/test_snap_installer.py @@ -1290,3 +1290,95 @@ def test_snaps_no_channel_raises_errors(fake_executor): brief="channel cannot be empty", resolution="set channel to a non-empty string or `None`", ) + + +# Regression test for: +# https://github.com/canonical/craft-providers/issues/445 + + +def test_assertions_include_developer_account_when_different_from_publisher( + fake_executor, + fake_process, + mocker, +): + """Assertion file must include the developer account when it differs from publisher. + + When a snap changes ownership (e.g. transferred from a personal account to an org), + the snap-revision assertion contains a ``developer-id`` that is different from the + current publisher's account. Without asserting the developer account, ``snap ack`` + fails with: 'cannot resolve prerequisite assertion: account ()'. + + Regression test for https://github.com/canonical/craft-providers/issues/445 + """ + publisher_id = "canonical-account-id" + developer_id = "original-developer-id" # Different from publisher_id! + + mocker.patch( + "craft_providers.actions.snap_installer.get_host_snap_info", + return_value=SnapInfo( + id="snap-id-abc123", + name="rockcraft", + revision="1194", + publisher=SnapPublisher(id=publisher_id), + ), + ) + + # The snap-revision assertion contains a developer-id that differs from publisher. + snap_revision_assertion = ( + b"type: snap-revision\n" + b"authority-id: canonical\n" + b"snap-sha3-384: abc123\n" + b"developer-id: " + developer_id.encode() + b"\n" + b"snap-id: snap-id-abc123\n" + b"snap-revision: 1194\n" + b"snap-size: 12345678\n" + b"timestamp: 2023-11-08T00:00:00Z\n" + b"\n" + ) + + # Register the four standard 'snap known' calls: + fake_process.register_subprocess( + ["snap", "known", "account-key", fake_process.any()] + ) + fake_process.register_subprocess( + ["snap", "known", "snap-declaration", fake_process.any()] + ) + fake_process.register_subprocess( + [ + "snap", + "known", + "snap-revision", + "snap-revision=1194", + "snap-id=snap-id-abc123", + ], + stdout=snap_revision_assertion, + ) + fake_process.register_subprocess( + ["snap", "known", "account", f"account-id={publisher_id}"] + ) + + # Bug: the developer's account is NOT fetched even though it's needed. + # After the fix, this call should also be registered. + fake_process.register_subprocess( + ["snap", "known", "account", f"account-id={developer_id}"] + ) + + fake_process.register_subprocess( + ["fake-executor", "snap", "ack", "/tmp/rockcraft.assert"] + ) + + snap_installer._add_assertions_from_host( + executor=fake_executor, + snap_name="rockcraft", + ) + + # Verify that the developer account assertion was fetched. + developer_account_calls = [ + call for call in fake_process.calls if f"account-id={developer_id}" in str(call) + ] + assert len(developer_account_calls) == 1, ( + f"Expected 1 call to fetch developer account '{developer_id}', " + f"got {len(developer_account_calls)}. " + "The developer's account assertion is required when developer-id " + "differs from the publisher-id in the snap-revision assertion." + ) From b0f09bbf2aabbe2363e52b1529e8387315f19751 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 05:26:56 +0000 Subject: [PATCH 2/3] fix: fetch developer account assertion when different from publisher (#445) Agent-Logs-Url: https://github.com/canonical/craft-providers/sessions/bef6730a-5abc-4b51-8aec-eedec9432721 Co-authored-by: lengau <4305943+lengau@users.noreply.github.com> --- craft_providers/actions/snap_installer.py | 34 +++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/craft_providers/actions/snap_installer.py b/craft_providers/actions/snap_installer.py index 5b1fd3fb8..35c429d74 100644 --- a/craft_providers/actions/snap_installer.py +++ b/craft_providers/actions/snap_installer.py @@ -249,6 +249,18 @@ def _get_assertion(query: list[str]) -> bytes: ) from error +def _get_developer_id_from_snap_revision(snap_revision_assertion: bytes) -> str | None: + """Parse the developer-id field from a snap-revision assertion. + + :param snap_revision_assertion: raw bytes of the snap-revision assertion + :returns: the developer-id value, or None if not present + """ + for line in snap_revision_assertion.splitlines(): + if line.startswith(b"developer-id:"): + return line.split(b":", 1)[1].strip().decode() + return None + + @contextlib.contextmanager def _get_assertions_file( snap_name: str, snap_id: str, snap_revision: str, snap_publisher_id: str @@ -264,22 +276,40 @@ def _get_assertions_file( as the target """ logger.debug("Creating an assert file for snap %r", snap_name) - assertion_queries = [ + + snap_revision_assertion = _get_assertion( + ["snap-revision", f"snap-revision={snap_revision}", f"snap-id={snap_id}"] + ) + + developer_id = _get_developer_id_from_snap_revision(snap_revision_assertion) + + assertion_queries: list[list[str]] = [ [ "account-key", "public-key-sha3-384=BWDEoaqyr25nF5SNCvEv2v" "7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0hqUel3m8ul", ], ["snap-declaration", f"snap-name={snap_name.partition('_')[0]}"], - ["snap-revision", f"snap-revision={snap_revision}", f"snap-id={snap_id}"], ["account", f"account-id={snap_publisher_id}"], ] + if developer_id and developer_id != snap_publisher_id: + logger.debug( + "Snap %r has developer-id %r different from publisher-id %r," + " fetching developer account assertion", + snap_name, + developer_id, + snap_publisher_id, + ) + assertion_queries.append(["account", f"account-id={developer_id}"]) + with temp_paths.home_temporary_file() as assert_file_path: with assert_file_path.open("wb") as assert_file: for query in assertion_queries: assert_file.write(_get_assertion(query)) assert_file.write(b"\n") + assert_file.write(snap_revision_assertion) + assert_file.write(b"\n") assert_file.flush() yield assert_file_path From 5df1922b81408ae484725ea6fd6d879e4cb46077 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 05:27:53 +0000 Subject: [PATCH 3/3] fix: handle UnicodeDecodeError in developer-id parsing Agent-Logs-Url: https://github.com/canonical/craft-providers/sessions/bef6730a-5abc-4b51-8aec-eedec9432721 Co-authored-by: lengau <4305943+lengau@users.noreply.github.com> --- craft_providers/actions/snap_installer.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/craft_providers/actions/snap_installer.py b/craft_providers/actions/snap_installer.py index 35c429d74..95b840dfc 100644 --- a/craft_providers/actions/snap_installer.py +++ b/craft_providers/actions/snap_installer.py @@ -257,7 +257,13 @@ def _get_developer_id_from_snap_revision(snap_revision_assertion: bytes) -> str """ for line in snap_revision_assertion.splitlines(): if line.startswith(b"developer-id:"): - return line.split(b":", 1)[1].strip().decode() + try: + return line.split(b":", 1)[1].strip().decode() + except UnicodeDecodeError: + logger.warning( + "Failed to decode developer-id from snap-revision assertion" + ) + return None return None