On this page
Monorepo sync command and helpers: trigger rewriting, working-directory injection, inlined CI router generation, import name detection, and tag prefixes.
#rlsbl.commands.monorepo.sync
#rlsbl.commands.monorepo.sync
Monorepo sync command and all sync helpers: working-directory injection, inline CI router generation.
#_inject_packages_dir
def _inject_packages_dir(doc, project_path)Add packages-dir to PyPI publish steps.
When working-directory is set, uv build creates artifacts in {project_path}/dist/ but the publish action looks for dist/ at the repo root. We inject packages-dir so it finds the right directory.
#count_reusable_workflow_calls
def count_reusable_workflow_calls(jobs)Count job-level uses: entries (reusable workflow calls) in a jobs mapping.
Step-level uses: (actions) are not counted -- only jobs whose mapping carries a top-level uses: key, which is what GitHub treats as a reusable workflow call.
#validate_router_reusable_calls
def validate_router_reusable_calls(jobs, router_name)Hard-error when a generated router carries >= 20 reusable workflow calls.
GitHub rejects workflow files with too many reusable-workflow calls outright -- the router never runs and CI silently stops. Generated routers must inline jobs instead of calling reusable workflows.
Raises:
ConfigError: when jobs containsGITHUB_MAX_REUSABLE_CALLSor
more job-level uses: entries.
#_strip_expression_wrapper
def _strip_expression_wrapper(expr)Strip an optional ${{ ... }} wrapper from a GitHub expression.
#_generate_router
def _generate_router(projects, filters)Generate ci-router.yml content with every project's CI jobs inlined.
Each project dict must carry _ci_docs: a list of (job_prefix, doc) pairs where doc is a parsed CI workflow (working-directory already injected) and job_prefix is the per-file key ({name}-ci or {name}-ci-{target}).
filters is a :class:rlsbl.router_filters.RouterFilters built over the WHOLE workspace, not just projects: a member's filter is derived from territories it does not own (its dependencies', and -- for the root member -- every other member's), so the derivation cannot be done from the CI-carrying subset alone.
GitHub rejects workflow files with 20+ reusable-workflow calls, so the router inlines every project's CI jobs directly instead of uses: calls. Each inlined job:
- gets its key prefixed with the CI file's job prefix (unique per file),
- gets an explicit
name: "{prefix} / {job}"so check-run names stay
identical to the reusable-workflow era (publish gate regexes and branch protection rules keep matching),
- is gated on
needs: detect+if: needs.detect.outputs.{project},
short-circuited by the run_all dispatch input (see :data:RUN_ALL_INPUT),
- keeps intra-workflow
needs:(rewritten to the prefixed keys).
#_get_monorepo_tag_prefix
def _get_monorepo_tag_prefix(project, root, releasables=None)Return the tag prefix for a monorepo project's publish router condition.
When releasables are provided and the project belongs to a releasable (releasable = "X"), the prefix is derived from the releasable's tag_format (e.g. "{name}@v{version}" -> "X@v").
Otherwise falls back to the target's monorepo_tag_glob (glob minus trailing *). For Go projects this yields go/v, for others name@v.
#_root_is_publisher
def _root_is_publisher(project, root)Return True when the root project (path='.') actually publishes.
A root publisher has publish_mode != "none" and at least one detectable publish target. Detection is config-based (not based on the on-disk publish.yml, which is the generated router itself). A ConfigError during publish_mode or target detection means the root is not a resolvable publisher -- treat it as a non-publisher rather than crashing sync.
#_member_suppresses_publish
def _member_suppresses_publish(project, root)Return True when project's effective config declares publish_mode: "none".
The publish router inlines each member's publish jobs verbatim, so a member that declares it must not publish has to be excluded BEFORE inlining -- otherwise a stale per-member publish.yml (e.g. a legacy PyPI job left behind when the repo went private) becomes a live registry publish in the generated root router.
publish_mode is read with releasable-level inheritance, mirroring :func:_root_is_publisher. A member with no resolvable publish_mode is NOT suppressed: absence of a declaration is not a declaration, and the publish-mode config check hard-errors on the missing key separately.
#_build_project_template_vars
def _build_project_template_vars(project_dir, root)Build a template vars dict for a project, with both namespaced and un-namespaced keys.
Detects the project's targets, calls each target's template_vars(), and returns a merged dict where each target's vars appear under both their bare names and {target_name}.{key} namespaced names. This allows process_template to resolve patterns like {{pypi.minRequiredPython}} in workflow comments.
#scaffold_releasable_dirs
def scaffold_releasable_dirs(workspace_root)Create the directory structure for each releasable in the workspace.
Every releasable declared in [[releasables]] gets:
.rlsbl-monorepo/releasables/{name}/version(user-owned, never overwritten).rlsbl-monorepo/releasables/{name}/changes/unreleased.jsonl(user-owned)
Hook scripts are no longer scaffolded here -- hooks are config-driven (see hooks key in config.json). Version and unreleased.jsonl are user-owned: created once, never overwritten.
Args:
workspace_root: path to the monorepo root.
Returns:
- A list of file paths that were created or updated (for commit tracking).
#_saferm_workflow
def _saferm_workflow(filepath, description)Delete a stale generated workflow file via saferm (audit trail).
Raises RuntimeError if saferm is not on PATH and propagates subprocess.CalledProcessError if saferm exits non-zero.
#_sync_import_names
def _sync_import_names(root, projects)Auto-populate import_name for Python projects whose import name differs from their project name.
For each Python project (has pyproject.toml with [project]), detects the package root via detect_python_package_root and compares the derived import name against the underscored project name. When they differ and import_name is not already set in workspace.toml, writes import_name to the project's entry.
Returns the workspace.toml path if it was modified, or None.