fix(release): CI wait polls the prod rung; escape the header tooltip apostrophe

get_latest_ci_conclusion defaults to the ladder's head rung, so
wait_for_ci searched slave for a release commit that lives on master
and timed out after 40 minutes with the run already green. The wait
now passes the prod branch explicitly. Also fixes the
react/no-unescaped-entities error that turned master's Panel CI red.
This commit is contained in:
Renn F
2026-07-16 06:59:22 +02:00
parent e590e64b86
commit 33cb4d7104
4 changed files with 41 additions and 3 deletions
+1 -1
View File
@@ -125,7 +125,7 @@ export function Header() {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
Signed in as the CEO the panel's single human operator Signed in as the CEO the panel&apos;s single human operator
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
+5 -1
View File
@@ -2202,6 +2202,7 @@ class GitService(BaseService):
*, *,
workflow: str | None = None, workflow: str | None = None,
head_sha: str | None = None, head_sha: str | None = None,
branch: str | None = None,
) -> dict[str, Any] | None: ) -> dict[str, Any] | None:
"""Latest completed CI (GitHub Actions) run on a project's default branch. """Latest completed CI (GitHub Actions) run on a project's default branch.
@@ -2229,7 +2230,10 @@ class GitService(BaseService):
git_token = await self._token_for_project(project_slug) git_token = await self._token_for_project(project_slug)
if not git_token: if not git_token:
return None return None
branch = head_branch(project) # Default to the head rung (where dev work and the release gate look);
# the release-commit CI wait overrides with the prod rung, where the
# pushed release commit actually lives.
branch = branch or head_branch(project)
query = _CiRunQuery( query = _CiRunQuery(
project_slug=project_slug, project_slug=project_slug,
owner_repo=(owner, repo), owner_repo=(owner, repo),
+4 -1
View File
@@ -449,7 +449,10 @@ class _GitReleaseOps:
git = get_git_service(self._session) git = get_git_service(self._session)
for _ in range(_CI_MAX_POLLS): for _ in range(_CI_MAX_POLLS):
ci = await git.get_latest_ci_conclusion( ci = await git.get_latest_ci_conclusion(
self._slug, workflow=self._ci_workflow, head_sha=commit_sha self._slug,
workflow=self._ci_workflow,
head_sha=commit_sha,
branch=self._default_branch,
) )
if ci and ci.get("head_sha") == commit_sha: if ci and ci.get("head_sha") == commit_sha:
conclusion = (ci.get("conclusion") or "").lower() conclusion = (ci.get("conclusion") or "").lower()
@@ -587,3 +587,34 @@ def test_insert_changelog_entry_without_unreleased_is_unchanged_behavior() -> No
result = re._insert_changelog_entry(existing, entry) result = re._insert_changelog_entry(existing, entry)
assert result.index("## [0.25.0]") < result.index("## [0.24.0]") assert result.index("## [0.25.0]") < result.index("## [0.24.0]")
assert "- old" in result and "- new" in result assert "- old" in result and "- new" in result
@pytest.mark.asyncio
async def test_wait_for_ci_polls_the_prod_branch(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""The release commit lives on the prod rung — the CI wait must query that
branch, not the ladder head where get_latest_ci_conclusion defaults."""
seen: dict[str, object] = {}
async def _fake_get_ci(_slug: str, **kwargs: object) -> dict[str, object]:
seen.update(kwargs)
return {"head_sha": "cafebabe", "conclusion": "success"}
monkeypatch.setattr(
"roboco.services.git.get_git_service",
lambda _session: SimpleNamespace(get_latest_ci_conclusion=_fake_get_ci),
)
ctx = _ReleaseContext(
slug="roboco-api",
prod_branch="master",
root=tmp_path,
git_url="x",
git_prefix=[],
ci_workflow="ci.yml",
env_chain=["slave"],
)
ops = _GitReleaseOps(session=MagicMock(), ctx=ctx)
ok = await ops.wait_for_ci("cafebabe")
assert ok is True
assert seen.get("branch") == "master"