On this page
Publish CI gate job that confirms, from the GitHub Release body, the CI verdict rlsbl obtained before tagging; collapses retried check-runs, fails forward.
#rlsbl.publish_gate
#rlsbl.publish_gate
Single source of truth for the publish CI gate, generating the GitHub Actions gate job that blocks publishing until all CI checks pass on the release commit.
Publish workflows trigger on release: published and workflow_dispatch and would otherwise RACE CI on the same commit -- a broken artifact could publish before CI reported. Every publish workflow therefore starts with a gate job, defined once here and injected everywhere:
- standalone templates get it via the
{{publishGate}}template variable
(rendered by :func:gate_job_template_snippet);
- the merged multi-target publish workflow embeds :func:
build_gate_job; - the monorepo publish router embeds :func:
build_router_gate_job.
The gate resolves the release commit MARKER-FIRST: it reads the exact commit CI ran on from the <!-- rlsbl-ci-sha: <40-hex> --> marker rlsbl writes into the GitHub Release body. A freshly created release can lag on a GitHub API read replica, so the marker read is RETRIED (GATE_MARKER_ATTEMPTS attempts, GATE_MARKER_RETRY_SECONDS apart) before concluding the marker is absent; only then does it fall back to $GITHUB_SHA (the tag's commit for both release: published and workflow_dispatch at the tag ref) for older releases that predate the marker. It never reads the release event payload (dispatch retries have none), so the resolution path is uniform for release events and dispatch retries alike. It then polls the GitHub checks API until the releasing project's CI check runs complete, collapsing retried same-named check-runs to the latest so a superseded failure cannot block.
Conclusion semantics (explicit, no silent waits):
successon every matching check -> the gate passes.failure/timed_out-> hard error (CI did not pass).cancelled/skipped-> hard error with an explanation; a
cancelled or skipped check proves nothing about the commit, so the gate never treats it as passing and never waits forever for it.
- No matching check runs after a grace window -> hard error; a scaffolded
repository always has CI, so the release commit must produce check runs.
Under rlsbl's main-as-candidate release ordering this gate is a CONFIRMATION, not a wait: rlsbl release run already waited for CI to conclude green on the candidate commit before tagging it and creating the Release, so the checks polled here are normally already complete. A red conclusion therefore means the world changed after the release (CI re-run and regressed, a required check added later) or the tag was created outside rlsbl -- which is why the remediation is "cut a new release and deprecate this one", never "retry the publish".
#publish_concurrency_block
def publish_concurrency_block() -> dictWorkflow-level concurrency mapping for publish workflows.
#ci_check_regex_for_targets
def ci_check_regex_for_targets(targets: list[str]) -> strBuild the check-run name regex for a set of scaffolded targets.
The regex matches each target's CI job names exactly, plus their matrix expansions (test (20)). Targets without a known CI template fall back to the conventional test job name.
#gate_targets_from_config
def gate_targets_from_config(config: dict | None, registry: str | None) -> list[str]Target names a standalone project's publish gate filter covers.
Config target entries may be dicts ({"name": ..., "path": ...}) for subdirectory targets; this reduces them to bare names and appends the primary registry. Shared by scaffold (which bakes the regex into the generated publish workflow) and the release CI gate (which must apply the SAME filter before tagging), so neither can gate on a different target set than the other.
#build_gate_job
def build_gate_job(check_regex: str | None=None, resolver_script: str | None=None) -> dictBuild the gate job as a workflow-jobs dict entry.
check_regex bakes the check-run name filter into the job env (standalone and merged workflows, where the releasing project is the repository itself). resolver_script prepends a step that resolves CI_CHECK_REGEX at runtime (the monorepo router, where the releasing project is derived from the tag ref).
#build_router_gate_job
def build_router_gate_job(prefix_regex_pairs: list[tuple[str, str]]) -> dictBuild the shared gate for the monorepo publish router.
prefix_regex_pairs maps each project's tag prefix to the regex matching its prefixed CI check-run names (the CI router invokes member CI as reusable workflows, so check runs are named <router job key> / <ci job name>). Longer prefixes are matched first so overlapping project names resolve to the right project.
#_literal_str_representer
def _literal_str_representer(representer, data)Represent multi-line strings with | literal block style.
#gate_job_template_snippet
def gate_job_template_snippet(check_regex: str) -> strRender the gate job as a YAML snippet for the {{publishGate}} var.
Returns the gate: job block indented two spaces so it slots directly under the jobs: key of a publish template. No trailing newline (the placeholder occupies its own line in the template).