Skip to content

Commit 86975a4

Browse files
fix(RELEASE-1993): fix missing result in update-fbc-catalog (#855)
Always write the iibLog result file even when the log URL is empty, write all five result files on early failure paths, and fetch full build details for reused builds so the logs field is populated. Assisted-by: Claude Code Signed-off-by: Filip Nikolovski <fnikolov@redhat.com>
1 parent 0ecbc12 commit 86975a4

2 files changed

Lines changed: 241 additions & 58 deletions

File tree

scripts/python/tasks/internal/test_update_fbc_catalog.py

Lines changed: 181 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,9 @@ def test_poll_and_collect_complete_success() -> None:
731731
"internal_index_image_copy": "internal/img:v1",
732732
"index_image": "registry/idx:v4.12",
733733
"from_index": "registry/idx:v4.12",
734+
}
735+
full_build: iib.IIBBuild = {
736+
**build,
734737
"logs": {"url": "https://logs/1"},
735738
}
736739
manifest = {
@@ -741,9 +744,12 @@ def test_poll_and_collect_complete_success() -> None:
741744
}
742745
],
743746
}
744-
with mock.patch(
745-
"skopeo.subprocess.run",
746-
return_value=_skopeo_result(json.dumps(manifest)),
747+
with (
748+
mock.patch("iib.get_build", return_value=full_build),
749+
mock.patch(
750+
"skopeo.subprocess.run",
751+
return_value=_skopeo_result(json.dumps(manifest)),
752+
),
747753
):
748754
result = update_fbc_catalog._poll_and_collect(
749755
"https://iib",
@@ -765,13 +771,14 @@ def test_poll_and_collect_failed_build() -> None:
765771
"state": "failed",
766772
"state_reason": "something broke",
767773
}
768-
result = update_fbc_catalog._poll_and_collect(
769-
"https://iib",
770-
build,
771-
3600,
772-
False,
773-
False,
774-
)
774+
with mock.patch("iib.get_build", return_value=build):
775+
result = update_fbc_catalog._poll_and_collect(
776+
"https://iib",
777+
build,
778+
3600,
779+
False,
780+
False,
781+
)
775782
assert result.exit_code == 1
776783
assert result.state == "failed"
777784

@@ -830,6 +837,12 @@ def _setup_result_env(
830837
return paths
831838

832839

840+
def _assert_all_results_exist(paths: dict[str, Path]) -> None:
841+
"""Assert that all five Tekton result files exist."""
842+
for name, path in paths.items():
843+
assert path.exists(), f"{name} result file was not created"
844+
845+
833846
def test_main_invalid_fbc_fragments(
834847
monkeypatch: pytest.MonkeyPatch,
835848
tmp_path: Path,
@@ -848,6 +861,7 @@ def test_main_invalid_fbc_fragments(
848861
state = json.loads(paths["RESULT_BUILD_STATE"].read_text(encoding="utf-8"))
849862
assert state["state"] == "failed"
850863
assert paths["RESULT_EXIT_CODE"].read_text(encoding="utf-8") == "1"
864+
_assert_all_results_exist(paths)
851865

852866

853867
def test_main_empty_fbc_fragments(
@@ -866,6 +880,7 @@ def test_main_empty_fbc_fragments(
866880
]
867881
)
868882
assert paths["RESULT_EXIT_CODE"].read_text(encoding="utf-8") == "1"
883+
_assert_all_results_exist(paths)
869884

870885

871886
def test_main_writes_results_on_success(
@@ -924,6 +939,30 @@ def test_main_writes_results_on_success(
924939
)
925940

926941

942+
def test_main_writes_iib_log_when_url_empty(
943+
monkeypatch: pytest.MonkeyPatch,
944+
tmp_path: Path,
945+
) -> None:
946+
"""All result files are written even when iib_log_url is empty."""
947+
paths = _setup_result_env(monkeypatch, tmp_path)
948+
_setup_mount_env(monkeypatch, tmp_path)
949+
950+
fake_result = update_fbc_catalog.RunResult(
951+
build_info={"id": 1, "state": "complete"},
952+
state="complete",
953+
state_reason="ok",
954+
index_image_digests="sha256:abc",
955+
iib_log_url="",
956+
exit_code=0,
957+
)
958+
with mock.patch("update_fbc_catalog.run", return_value=fake_result):
959+
rc = update_fbc_catalog.main(["--fbc-fragments", '["frag"]', "--from-index", "idx:v1"])
960+
961+
assert rc == 0
962+
_assert_all_results_exist(paths)
963+
assert paths["RESULT_IIB_LOG"].read_text(encoding="utf-8") == ""
964+
965+
927966
def test_main_check_step_error_writes_failure(
928967
monkeypatch: pytest.MonkeyPatch,
929968
tmp_path: Path,
@@ -961,6 +1000,7 @@ def test_main_check_step_error_writes_failure(
9611000
assert paths["RESULT_EXIT_CODE"].read_text(encoding="utf-8") == "1"
9621001
state = json.loads(paths["RESULT_BUILD_STATE"].read_text(encoding="utf-8"))
9631002
assert state["state"] == "failed"
1003+
_assert_all_results_exist(paths)
9641004

9651005

9661006
def test_main_missing_result_env_exits() -> None:
@@ -994,6 +1034,7 @@ def test_main_invalid_build_tags_json(
9941034
]
9951035
)
9961036
assert paths["RESULT_EXIT_CODE"].read_text(encoding="utf-8") == "1"
1037+
_assert_all_results_exist(paths)
9971038

9981039

9991040
def test_main_returns_nonzero_on_failed_build(
@@ -1030,6 +1071,7 @@ def test_main_returns_nonzero_on_failed_build(
10301071
state = json.loads(paths["RESULT_BUILD_STATE"].read_text(encoding="utf-8"))
10311072
assert state["state"] == "failed"
10321073
assert state["state_reason"] == "Build failed with exit code 1"
1074+
_assert_all_results_exist(paths)
10331075

10341076

10351077
def test_main_timeout_returns_124(
@@ -1063,6 +1105,7 @@ def test_main_timeout_returns_124(
10631105

10641106
assert rc == 124
10651107
assert paths["RESULT_EXIT_CODE"].read_text(encoding="utf-8") == "124"
1108+
_assert_all_results_exist(paths)
10661109

10671110

10681111
def _setup_mount_env(
@@ -1144,13 +1187,14 @@ def test_poll_and_collect_validation_error() -> None:
11441187
"index_image": "registry/idx:wrong",
11451188
"from_index": "registry/idx:v4.12",
11461189
}
1147-
result = update_fbc_catalog._poll_and_collect(
1148-
"https://iib",
1149-
build,
1150-
3600,
1151-
True,
1152-
True,
1153-
)
1190+
with mock.patch("iib.get_build", return_value=build):
1191+
result = update_fbc_catalog._poll_and_collect(
1192+
"https://iib",
1193+
build,
1194+
3600,
1195+
True,
1196+
True,
1197+
)
11541198
assert result.exit_code == 1
11551199
assert result.state == "failed"
11561200
assert "Index image mismatch" in result.state_reason
@@ -1164,13 +1208,14 @@ def test_poll_and_collect_missing_internal_copy() -> None:
11641208
"index_image": "registry/idx:v4.12",
11651209
"from_index": "registry/idx:v4.12",
11661210
}
1167-
result = update_fbc_catalog._poll_and_collect(
1168-
"https://iib",
1169-
build,
1170-
3600,
1171-
True,
1172-
True,
1173-
)
1211+
with mock.patch("iib.get_build", return_value=build):
1212+
result = update_fbc_catalog._poll_and_collect(
1213+
"https://iib",
1214+
build,
1215+
3600,
1216+
True,
1217+
True,
1218+
)
11741219
assert result.exit_code == 1
11751220
assert "Missing internal_index_image_copy" in result.state_reason
11761221

@@ -1182,9 +1227,12 @@ def test_poll_and_collect_digest_extraction_fails() -> None:
11821227
"state": "complete",
11831228
"internal_index_image_copy": "internal/img:v1",
11841229
}
1185-
with mock.patch(
1186-
"skopeo.subprocess.run",
1187-
return_value=_skopeo_result(returncode=1),
1230+
with (
1231+
mock.patch("iib.get_build", return_value=build),
1232+
mock.patch(
1233+
"skopeo.subprocess.run",
1234+
return_value=_skopeo_result(returncode=1),
1235+
),
11881236
):
11891237
result = update_fbc_catalog._poll_and_collect(
11901238
"https://iib",
@@ -1241,26 +1289,28 @@ def test_poll_and_collect_failed_state_reason() -> None:
12411289
"state": "failed",
12421290
"state_reason": "IIB internal error",
12431291
}
1244-
result = update_fbc_catalog._poll_and_collect(
1245-
"https://iib",
1246-
build_with_reason,
1247-
3600,
1248-
False,
1249-
False,
1250-
)
1292+
with mock.patch("iib.get_build", return_value=build_with_reason):
1293+
result = update_fbc_catalog._poll_and_collect(
1294+
"https://iib",
1295+
build_with_reason,
1296+
3600,
1297+
False,
1298+
False,
1299+
)
12511300
assert result.state_reason == "IIB internal error"
12521301

12531302
build_no_reason: iib.IIBBuild = {
12541303
"id": 2,
12551304
"state": "failed",
12561305
}
1257-
result2 = update_fbc_catalog._poll_and_collect(
1258-
"https://iib",
1259-
build_no_reason,
1260-
3600,
1261-
False,
1262-
False,
1263-
)
1306+
with mock.patch("iib.get_build", return_value=build_no_reason):
1307+
result2 = update_fbc_catalog._poll_and_collect(
1308+
"https://iib",
1309+
build_no_reason,
1310+
3600,
1311+
False,
1312+
False,
1313+
)
12641314
assert result2.state_reason == "Build failed with exit code 1"
12651315

12661316

@@ -1627,6 +1677,7 @@ def test_main_empty_from_index(
16271677
]
16281678
)
16291679
assert paths["RESULT_EXIT_CODE"].read_text(encoding="utf-8") == "1"
1680+
_assert_all_results_exist(paths)
16301681

16311682

16321683
def test_poll_and_collect_removes_state_history_without_polling() -> None:
@@ -1637,6 +1688,13 @@ def test_poll_and_collect_removes_state_history_without_polling() -> None:
16371688
"internal_index_image_copy": "internal/img:v1",
16381689
"state_history": [{"state": "in_progress"}],
16391690
}
1691+
full_build: iib.IIBBuild = {
1692+
"id": 1,
1693+
"state": "complete",
1694+
"internal_index_image_copy": "internal/img:v1",
1695+
"state_history": [{"state": "in_progress"}],
1696+
"logs": {"url": "https://logs/1"},
1697+
}
16401698
manifest = {
16411699
"manifests": [
16421700
{
@@ -1645,9 +1703,12 @@ def test_poll_and_collect_removes_state_history_without_polling() -> None:
16451703
}
16461704
],
16471705
}
1648-
with mock.patch(
1649-
"skopeo.subprocess.run",
1650-
return_value=_skopeo_result(json.dumps(manifest)),
1706+
with (
1707+
mock.patch("iib.get_build", return_value=full_build),
1708+
mock.patch(
1709+
"skopeo.subprocess.run",
1710+
return_value=_skopeo_result(json.dumps(manifest)),
1711+
),
16511712
):
16521713
result = update_fbc_catalog._poll_and_collect(
16531714
"https://iib",
@@ -1658,3 +1719,79 @@ def test_poll_and_collect_removes_state_history_without_polling() -> None:
16581719
)
16591720
assert result.exit_code == 0
16601721
assert "state_history" not in result.build_info
1722+
1723+
1724+
def test_poll_and_collect_fetches_full_details_for_reused_build() -> None:
1725+
"""Reused completed build is re-fetched via the detail endpoint."""
1726+
list_build: iib.IIBBuild = {
1727+
"id": 42,
1728+
"state": "complete",
1729+
"internal_index_image_copy": "internal/img:v1",
1730+
}
1731+
detail_build: iib.IIBBuild = {
1732+
"id": 42,
1733+
"state": "complete",
1734+
"internal_index_image_copy": "internal/img:v1",
1735+
"logs": {"url": "https://logs/42"},
1736+
}
1737+
manifest = {
1738+
"manifests": [
1739+
{
1740+
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
1741+
"digest": "sha256:abc",
1742+
}
1743+
],
1744+
}
1745+
with (
1746+
mock.patch("iib.get_build", return_value=detail_build) as get_mock,
1747+
mock.patch(
1748+
"skopeo.subprocess.run",
1749+
return_value=_skopeo_result(json.dumps(manifest)),
1750+
),
1751+
):
1752+
result = update_fbc_catalog._poll_and_collect(
1753+
"https://iib",
1754+
list_build,
1755+
3600,
1756+
False,
1757+
False,
1758+
)
1759+
get_mock.assert_called_once_with("https://iib", 42)
1760+
assert result.iib_log_url == "https://logs/42"
1761+
assert result.exit_code == 0
1762+
1763+
1764+
def test_poll_and_collect_detail_fetch_failure_is_non_fatal() -> None:
1765+
"""Failure to fetch full details does not break the flow."""
1766+
list_build: iib.IIBBuild = {
1767+
"id": 42,
1768+
"state": "complete",
1769+
"internal_index_image_copy": "internal/img:v1",
1770+
}
1771+
manifest = {
1772+
"manifests": [
1773+
{
1774+
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
1775+
"digest": "sha256:abc",
1776+
}
1777+
],
1778+
}
1779+
with (
1780+
mock.patch(
1781+
"iib.get_build",
1782+
side_effect=requests.ConnectionError("timeout"),
1783+
),
1784+
mock.patch(
1785+
"skopeo.subprocess.run",
1786+
return_value=_skopeo_result(json.dumps(manifest)),
1787+
),
1788+
):
1789+
result = update_fbc_catalog._poll_and_collect(
1790+
"https://iib",
1791+
list_build,
1792+
3600,
1793+
False,
1794+
False,
1795+
)
1796+
assert result.exit_code == 0
1797+
assert result.iib_log_url == ""

0 commit comments

Comments
 (0)