On this page
Scaffold command that generates and updates release infrastructure from templates with three-way merge, CI workflows, hooks, and multi-target support.
#rlsbl.commands.init_cmd
#rlsbl.commands.init_cmd
Init command that scaffolds release infrastructure from templates, creating CI workflows, hooks, changelog, and config files.
#_check_npm_lockfile_missing
def _check_npm_lockfile_missing()Check if any npm lockfile exists from cwd up to the git root.
Returns True if no lockfile is found (i.e., lockfile is missing). Prints a warning to stderr when missing.
#file_hash
def file_hash(path)SHA-256 hash of a file's contents.
#load_hashes
def load_hashes()Load stored file hashes from .rlsbl/hashes.json.
#save_hashes
def save_hashes(hashes)Write file hashes to .rlsbl/hashes.json.
#_ensure_target_in_config
def _ensure_target_in_config(registry_name)Add registry_name to the targets array in .rlsbl/config.json if not already present.
#process_template
def process_template(template_content, vars_dict, template_path=None)Process a template string with a two-pass substitution.
Pass 1 resolves {{action "owner/name"}} placeholders against the central action-version table (rlsbl/data/action_versions.toml). An unknown action raises :class:UnknownActionError immediately -- no implicit defaults.
Pass 2 resolves the existing {{varName}} (and dotted {{a.b}}) placeholders against vars_dict.
Returns (content, unreplaced) where unreplaced is the list of variable names in pass 2 that had no entry in vars_dict. Pass 1 misses raise instead of being collected.
#_save_base
def _save_base(target, content)Save rendered template content as the merge base for future three-way merges.
#_load_base
def _load_base(target)Load the stored merge base for a target file. Returns None if not stored.
#_three_way_merge
def _three_way_merge(ours_text, base_text, theirs_text)Three-way merge using git merge-file.
Writes three temp files in the project dir (not /tmp), runs git merge-file -p ours base theirs, and returns (merged_text, has_conflicts). Exit code: 0 = clean merge, positive = number of conflicts, negative = error.
#plan_mappings
def plan_mappings(template_dir, mappings, vars_dict, force, update=False)Compute what process_mappings would do, without writing anything.
Returns a list of plan dicts. Each plan represents one mapping and contains: - "target": the target file path - "status": one of "new", "updated", "unchanged", "skipped", "user-owned", or a string starting with "CONFLICTS"; or status values like "overwritten", "created", "merged", "updated (additive merge)", "year updated (...)" -- the same vocabulary the original function produced for the (created, skipped) lists. - "bucket": "created" or "skipped" -- which result list this entry belongs in - "action": one of "write", "save_base_only", "license_year_update", "gitignore_merge", "merge_write", "none". Tells apply_plans what to do. - "content": the bytes to write (when action requires it). None otherwise. - "base_content": template content to save as the new merge base. None when no base should be saved this run. - "warning": optional extra warning string emitted alongside this plan - "unreplaced": list of unreplaced template var names (for warnings) - "year_update": for license_year_update, a dict with "current_year", "old_year" so apply can recompute the new content - "additive_lines": for gitignore_merge, the lines to append - "existing_content": for gitignore_merge, the original content - "template_not_found": True for warning-only entries
#apply_plans
def apply_plans(plans)Apply a list of plans from plan_mappings, performing all side effects.
Returns (created, skipped, warnings, new_hashes) matching the original process_mappings return shape.
#process_mappings
def process_mappings(template_dir, mappings, vars_dict, force, update=False, existing_hashes=None)Process a list of template mappings: read each template, apply vars, write target files.
Uses a universal three-way merge (via git merge-file) for existing files: base (last scaffolded version) + ours (user's current file) + theirs (new template). USER_OWNED files are never overwritten or merged (except LICENSE year update).
Returns (created, skipped, warnings, new_hashes). created/skipped are lists of (target, status) tuples for unified display.
Implemented as plan_mappings() (pure analysis) + apply_plans() (side effects).
#_print_file_status_table
def _print_file_status_table(created, skipped)Print the unified file list table with dot-padded status column.
#_print_dry_run_report
def _print_dry_run_report(plans_groups, registry=None, registries=None)Print the file status table from plans without applying them.
plans_groups is a list of plan lists (registry plans, shared plans, etc.).
#_install_or_update_pre_push_hook
def _install_or_update_pre_push_hook()Install the rlsbl pre-push hook, upgrading older versions in place.
See rlsbl/hook_hashes.py for the historical hash set.
Behavior: - .git missing -> no-op - hook missing -> write current template, chmod 755 - hook matches current hash -> no-op (already up to date) - hook matches old known hash -> overwrite, print upgrade notice - hook hash unknown -> skip, print warning + unified diff
#_finalize_scaffold
def _finalize_scaffold(existing_hashes, all_hash_dicts, created, skipped, warnings, registry=None, flags=None, registries=None, npm_lockfile_missing=False)Shared post-processing for scaffold: chmod, hooks, version marker, hashes, tagging, summary.
all_hash_dicts is a list of dicts to merge into existing_hashes. flags is the CLI flags dict (used for tagging check). registries is a list of registry names (used for tagging). npm_lockfile_missing: if True, prepend a lockfile step to npm next steps.
#_resolve_private
def _resolve_private(flags)Determine if this is a private repository.
Checks --private flag first, then saved config, then auto-detects via GitHub API.
On --update: if private is missing from config and no --private flag was passed, prints an error and exits. The user must add the key explicitly.
On new scaffold: auto-detects if needed and returns the result. The caller is responsible for persisting the value to config.json.
Returns True/False, or False if detection fails (new scaffold only).
#_filter_mappings_for_private
def _filter_mappings_for_private(mappings)Remove publish template mappings (private repos don't publish to registries).
#_append_deploy_workflow_if_configured
def _append_deploy_workflow_if_configured(mappings)Add deploy workflow template to mappings if deploy config exists.
#_print_private_summary
def _print_private_summary()Print helpful output for private repository scaffold.
#_trigger_monorepo_sync
def _trigger_monorepo_sync(no_commit=False)If the current directory is inside a monorepo workspace, run sync.
Uses a subprocess so that sys.exit() calls inside sync don't kill scaffold. Failures are silently ignored -- sync is best-effort after scaffold.
When no_commit is True, propagates --no-commit to the sync call so a single user invocation with --no-commit produces zero commits.
#run_cmd
def run_cmd(registry, args, flags)Init command handler.
Scaffolds release infrastructure (CI, publish workflows, changelog, etc.) from templates.
#_extract_top_level_block
def _extract_top_level_block(lines, key)Extract a top-level YAML block (e.g., 'permissions:', 'env:') from template lines.
Returns (block_lines, remaining_lines) where block_lines are the key + its indented children, and remaining_lines are everything else.
#_parse_permissions
def _parse_permissions(block_lines)Parse permission key-value pairs from a permissions block.
Returns a dict like {"contents": "write", "id-token": "write"}.
#_parse_env
def _parse_env(block_lines)Parse env key-value pairs from an env block.
Returns a list of (key, full_line) tuples to preserve formatting. Keys are used for deduplication; full lines are used for output.
#_merge_permissions
def _merge_permissions(perm_dicts)Merge multiple permission dicts, choosing the most permissive value for each key.
Permission escalation order: read < write.
#_extract_jobs_section
def _extract_jobs_section(lines)Extract the content under the 'jobs:' key from template lines.
Returns lines starting from the first job definition (the indented content after 'jobs:'), not including the 'jobs:' line itself.
#_generate_merged_publish
def _generate_merged_publish(targets, template_vars)Generate a merged publish.yml from individual target publish templates.
Reads each target's publish.yml.tpl, extracts jobs/permissions/env, and composes a single workflow with all jobs merged.
#_merge_template_vars
def _merge_template_vars(registries_list, primary, target_paths)Build a merged template vars dict with namespaced keys from all targets.
The primary target's vars are included un-namespaced (as the base). Every target's vars are also included with a namespace prefix: {target_name}.{key} so templates can reference target-specific values like {{pypi.minRequiredPython}}.
target_paths is a dict mapping target name to its directory path.
#_plan_merged_publish
def _plan_merged_publish(publish_target, merged_content, force, update)Compute a plan for the merged publish workflow (analysis only).
#run_cmd_multi
def run_cmd_multi(registries_list, args, flags)Scaffold for multiple registries with a merged publish workflow.
Uses the primary registry for template vars and CI, then writes a merged publish.yml that contains jobs for all detected registries.