rlsbl v0.113.0 /rlsbl.commands.monorepo.publish_inline
On this page

Inline publish logic for monorepo projects: GitHub Actions workflow parsing, job extraction, trigger rewriting, and YAML emission.

#rlsbl.commands.monorepo.publish_inline

#rlsbl.commands.monorepo.publish_inline

Inline publish logic for monorepo projects: workflow parsing and YAML emission.

#parse_publish_workflow

python
def parse_publish_workflow(path: str) -> dict

Parse a GitHub Actions publish workflow file.

Reads the YAML file at path, validates it has a jobs: key, and returns a dict with the top-level keys that matter for inline publish generation.

Returns a dict with keys: jobs -- the jobs mapping from the workflow permissions -- workflow-level permissions mapping, or None env -- workflow-level env mapping, or None name -- workflow name string, or None

#parse_publish_workflow_content

python
def parse_publish_workflow_content(content: str, source: str='<string>') -> dict

Parse a publish workflow from a YAML string.

Same contract as :func:parse_publish_workflow but reads from an in-memory string. Used when the workflow is rendered from templates (root publisher) rather than read from disk.

#_literal_str_representer

python
def _literal_str_representer(representer, data)

Represent multi-line strings with | literal block style.

#emit_workflow

python
def emit_workflow(workflow_dict: dict) -> str

Emit a workflow dict as a YAML string.

Uses literal block style (|) for multi-line strings and preserves key order. The custom representer is registered on a private Dumper subclass so the global yaml state is never modified.

#prefix_jobs

python
def prefix_jobs(project_name: str, jobs: dict) -> dict

Prefix every job key with {project_name}- and rewrite needs: references.

needs: gate references are preserved as-is: the router emits ONE shared gate job, so member references to it must never be force-prefixed (a {project}-gate need would dangle).

Returns a new dict; the original jobs is not mutated.

#inject_job_metadata

python
def inject_job_metadata(jobs: dict, tag_prefix: str, working_dir: str) -> dict

Add if: condition and defaults.run.working-directory to every job.

Returns a new dict; the original jobs is not mutated.

#_compose_project_subpath

python
def _compose_project_subpath(project_path: str, sub: str) -> str

Anchor a per-target sub path under project_path.

Mirrors :func:inject_job_metadata's working-directory composition, but string-based so packages-dir's trailing dist/ slash is preserved (os.path.normpath would strip it, and consumers/tests rely on it).

The merged-publish generator pre-injects each subdir target's own subpath into these inputs (e.g. py/dist/ for a pypi target living in py/). This composes that subpath under the project's path rather than overwriting it with a root-anchored value. Behaviour is unchanged for targets at the project root (sub already correct relative to the repo root) and for inputs already anchored under project_path (idempotent -- never double-prefixed).

#rewrite_action_paths

python
def rewrite_action_paths(jobs: dict, project_path: str) -> dict

Rewrite action inputs that contain file paths so they are relative to project_path.

Handles:

  • pypa/gh-action-pypi-publish: composes with.packages-dir (default

dist/) under project_path, preserving any per-target subpath the merged-publish generator pre-injected.

  • actions/setup-{go,python,node}: composes version-file paths under

project_path, likewise preserving pre-injected per-target subpaths.

Returns a new dict; the original jobs is not mutated.

#resolve_permissions

python
def resolve_permissions(jobs: dict, workflow_permissions: dict | None) -> dict

Push workflow-level permissions down to jobs that lack their own.

  • Jobs with an explicit permissions: key keep it unchanged.
  • Jobs without permissions: inherit workflow_permissions (if non-None).
  • If workflow_permissions is None and the job has no permissions, nothing is added.

Returns a new dict; the original jobs is not mutated.

#transform_project_jobs

python
def transform_project_jobs(project_name: str, project_path: str, tag_prefix: str, workflow_path: str) -> dict

Parse a sub-project's publish workflow and transform its jobs for the monorepo router.

Applies all transforms in the correct order:

  1. strip the member's own gate job (the router emits ONE shared gate)
  2. resolve_permissions (before prefixing — references original job structure)
  3. rewrite_action_paths
  4. inject_job_metadata
  5. prefix_jobs (last — changes keys; needs: gate is preserved)
  6. ensure every inlined job depends on the shared gate

Returns a dict of transformed jobs ready for merging into the root workflow.

#transform_parsed_jobs

python
def transform_parsed_jobs(project_name: str, project_path: str, tag_prefix: str, jobs: dict, permissions: dict | None) -> dict

Transform already-parsed publish jobs for the monorepo router.

Shared by :func:transform_project_jobs (member workflows read from disk) and the root-publisher path (jobs rendered from templates). See :func:transform_project_jobs for the full ordered transform contract.

#_require_root_publish_gate_regex

python
def _require_root_publish_gate_regex(project: dict, root: str) -> str

Return the mandatory publish_gate_check_regex for a root publisher.

Unlike sub-project members -- whose CI runs through the generated ci-router.yml with known <prefix> / <job> check-run names -- the root package's CI lives in a hand-authored workflow whose check-run names rlsbl cannot infer. The gate must match check runs that actually execute at the release SHA, so the regex is a REQUIRED config key with no default (this is a security gate; a wrong or absent regex would let publishes run ungated).

Raises ConfigError (hard error) when the key is missing.

#_render_root_publisher_jobs

python
def _render_root_publisher_jobs(project: dict, root: str, tag_prefix: str) -> dict

Render the root publisher's publish jobs from config/templates.

The root's publish.yml IS the router output, so it must never be read as a source (source==destination, and the transform pipeline is not idempotent on its own output). Instead the jobs are rendered from the project's pipeline config using the SAME standalone publish-template rendering the scaffold uses (_generate_merged_publish), then fed through the shared transform pipeline like every other member. This makes subsequent syncs regenerate from config, never from the router output -- idempotent by construction.

#_root_job_prefix

python
def _root_job_prefix(project: dict, root: str) -> str

Return a valid GitHub job-key prefix for the root publisher.

A root project's derived name is the basename of its path (".") -- not a valid job ID (GitHub job IDs must match [A-Za-z_][A-Za-z0-9_-]*). Use the releasable name when the root belongs to one (explicit mode), else the repository directory name.

#generate_inline_publish_router

python
def generate_inline_publish_router(projects_with_publish: list, root: str, releasables=None) -> str

Generate a monorepo publish router with all sub-project jobs inlined.

Instead of calling per-project reusable workflows via workflow_call, this inlines every sub-project's publish jobs directly into a single publish.yml. Each job gets an if: startsWith(github.ref_name, ...) condition so only the relevant project's jobs run on a given release -- ref-based so a workflow_dispatch retry at the tag ref hits the same jobs as the original release event.

A single shared gate job blocks all inlined publish jobs until the releasing project's CI check runs (resolved from the tag ref) conclude successfully. Member gate jobs are stripped during inlining.

When releasables are provided, tag prefixes are derived from the releasable's tag_format instead of the target's monorepo_tag_glob.

Returns the complete YAML string, ready to write to disk.

#compute_publish_hashes

python
def compute_publish_hashes(projects: list, root: str) -> dict

Compute SHA256 hashes of each project's publish workflow.

Returns a dict mapping project name to the hex digest of its publish.yml content, or None if the project has no publish workflow.

A reserved __rlsbl_version__ key carries the current rlsbl version. Member publish.yml hashes don't change when only the router generator changes across an rlsbl upgrade, so without this key a version bump that alters router output would silently skip regeneration. Seeding the version into the cache structure invalidates the cache on any rlsbl version change.

#load_publish_cache

python
def load_publish_cache(monorepo_dir: str) -> dict | None

Load the publish hash cache from monorepo_dir.

Returns the parsed dict, or None if the cache file does not exist or contains invalid JSON.

#save_publish_cache

python
def save_publish_cache(monorepo_dir: str, hashes: dict) -> str

Write the publish hash cache to monorepo_dir.

Returns the absolute path to the written cache file.

#should_regenerate_router

python
def should_regenerate_router(cached: dict | None, current: dict, router_path: str) -> bool

Decide whether the publish router needs regeneration.

Returns False (skip) only when cached matches current exactly AND router_path exists on disk. Any mismatch -- missing cache, changed hash, added/removed project, missing router file -- returns True.

Search