On this page
Base class for release targets providing shared defaults for version reading, writing, detection, scaffolding, and publish configuration.
#rlsbl.targets.base
#rlsbl.targets.base
Base class for release targets providing shared defaults for version reading, writing, detection, scaffolding, and publish configuration.
#TemplateVars
Dict subclass that auto-generates namespaced {target}.{key} entries.
On construction, for every key in base_dict, an additional entry "{target_name}.{key}" is stored so templates can reference target-specific values like {{pypi.minRequiredPython}}.
Post-construction mutations (tv["newkey"] = val) produce bare-only keys -- this is correct for non-target-specific additions like year or repoName that callers add after the target returns its vars.
#BaseTarget
Concrete base providing defaults for optional Protocol methods.
Subclasses should override detection_files with the filenames whose existence in a directory indicates a project of that type. The tuple is used both by the target's own detect() method and by checks.PROJECT_MANIFESTS (derived automatically from the registry).
#name
def name(self)Target registry name. Subclasses must override.
#detect
def detect(self, dir_path)Return True when any declared detection_files entry exists here.
This is the declared-manifest half of detection, and it is the whole story for a target whose presence is decided by a filename: npm by package.json, Go by go.mod, and so on. Those targets declare their filenames and inherit this method rather than restating the same os.path.exists call.
Targets whose presence depends on file CONTENT -- Flutter and Dart sharing pubspec.yaml, an Android application versus a Gradle library sharing build.gradle -- override this and inspect the file. A target that declares no detection files never auto-detects.
#version_file
def version_file(self, dir_path=None)Return the relative path of the file that holds the project version.
#tag_format
def tag_format(self, version)Return the git tag string for a standalone release version.
#monorepo_tag_format
def monorepo_tag_format(self, name, version, path=None)Return the git tag string for a monorepo package release.
#monorepo_tag_glob
def monorepo_tag_glob(self, name, path=None)Return a glob pattern matching all version tags for a monorepo package.
#template_dir
def template_dir(self)Return the path to this target's ecosystem-specific template directory.
#shared_template_dir
def shared_template_dir(self)Return the path to the shared template directory common to all targets.
#read_name
def read_name(self, dir_path, ctx)Read the project name from the target's manifest file.
#read_metadata
def read_metadata(self, dir_path)Read project metadata (license, description) from the manifest file.
The default is empty, and that is the right answer for every ecosystem whose manifest carries no license or description (Go modules, Swift packages, deno.json, Dockerfiles, ...). Those targets do NOT override this to return an empty dict of their own: not overriding it is what makes supports_read_metadata answer honestly.
#template_vars
def template_vars(self, dir_path, ctx)Return template variables extracted from the project for scaffold rendering.
#template_mappings
def template_mappings(self, ctx)Return the list of target-specific template-to-file mappings for scaffolding.
#shared_template_mappings
def shared_template_mappings(self, ctx)Return template-to-file mappings shared across all targets.
#_lint_config_mappings
def _lint_config_mappings(self, ctx)Return lint config mappings filtered by declared targets.
If no targets are configured, all 3 lint configs are included for backward compatibility with unconfigured projects.
#_extract_target_names
def _extract_target_names(ctx)Extract target name strings from ctx.config["targets"].
Returns a set of target names, or an empty set if targets is not configured or ctx is unavailable.
#check_project_exists
def check_project_exists(self, dir_path)Return True if the project's manifest file exists in dir_path.
#get_project_init_hint
def get_project_init_hint(self)Return a user-facing hint for initializing a project of this target type.
#write_version
def write_version(self, dir_path, version, ctx)Write a new version to the target's version file(s).
Returns a list of relative file paths (relative to dir_path) that were modified. Subclasses must override this method and return the actual paths written.
#_resolve_build_timeout
def _resolve_build_timeout(self, config)Resolve the build timeout from config, then the shipped default.
config["build_timeout"]-- an int, or a dict keyed by target
name with an optional "default" entry
self.BUILD_TIMEOUT_DEFAULTclass variable
There is deliberately no environment-variable layer: build budgets are declared in .rlsbl/config.json, never picked up from the ambient environment.
#build
def build(self, dir_path, version, *, config=None)Build distributable artifacts for this target. No-op by default.
#companion_tags
def companion_tags(self, name, version, path=None)Return additional tags to create alongside the primary release tag.
Ecosystems that require extra tags (e.g. Go module proxy tags) override this to return a list of tag strings. The default implementation returns no companion tags.
Args:
name: the releasable or project name.version: the version being released (withoutvprefix).path: workspace-relative path to the package directory, or
None for standalone projects.
Returns:
- List of tag strings to create alongside the primary tag.
#expected_refs
def expected_refs(self, version, context)Every git ref version owns: the primary tag, companions, aliases.
THE single authority for the question. The release flow creates and pushes exactly this set, and the unpublished-refs check renders exactly this set against the repository and its remote -- one derivation, so a ref the release creates can never be a ref the check does not look for.
context is a :class:~rlsbl.targets.refs.RefContext built by :func:~rlsbl.targets.refs.ref_context. Returns an :class:~rlsbl.targets.refs.ExpectedRefs.
Not overridden by any target: the per-target facts it composes (tag_format, monorepo_tag_format, companion_tags) are the axes, and this is the assembly of them.
#_primary_ref
def _primary_ref(self, version, context)The one tag the release itself is named after.
Three naming authorities, in precedence order: a releasable's declared tag_format, a monorepo package's target-derived monorepo_tag_format, and a standalone repository's tag_format.
#_companion_refs
def _companion_refs(self, version, context, primary)The extra tags this release's members' ecosystems require.
Only a releasable release has members to ask, which is why member_package_paths being None -- rather than empty -- means "no companions", exactly as the release flow's own guard did.
Two rules, both inherited from the collector this replaced:
- A primary tag that is ALREADY Go-compatible (it contains
/v)
suppresses companions entirely, so a release already tagged that way does not duplicate its own tag.
- A publish-suppressed member (
publish_mode: "none") contributes
nothing -- there is no proxy to satisfy for something never published.
A member whose config cannot be resolved is a HARD ERROR, matching the version-sync plan: the two must agree on the member set, and silently skipping one here would tag a release the sync path would have refused.
#normalize_package_name
def normalize_package_name(self, raw_name)Reduce a package name to the form this registry compares by.
Registries differ in what they consider "the same name": PyPI folds runs of -_. to a single hyphen (PEP 503), npm removes them entirely, Go compares the last path segment of a module path. A cross-target name-consistency check must ask each target rather than keep a dict keyed by target name.
The default lowercases, which is the right answer for a registry with no normalization rules of its own.
#query_latest_version
def query_latest_version(self, name)Ask this target's registry for the latest published version.
Returns a dict with status "found" (plus version), "not_found", or "error" (plus message) -- the shape rlsbl.registry has always used.
The default answers error naming the target rather than returning None: a caller comparing a local version against "the registry" must never mistake "this ecosystem has no version API" for "the package is unpublished".
#claim_placeholder
def claim_placeholder(self, name, tmpdir)Publish a minimal placeholder package to reserve name.
Targets whose registry accepts a publish override this. The default raises: a target that cannot claim a name must not be reachable from rlsbl claim-name, and claimable_targets() derives the command's accepted set from exactly this method.
#registry_display_name
def registry_display_name(self)How to spell this target's registry in user-facing output.
Defaults to the target name, which is already right for npm and most others. PyPI capitalises and Go's index has a different name entirely, so they override. This replaced a display dict keyed by target name.
#format_version
def format_version(self, version)Format a semver version for this target's ecosystem.
The default implementation returns the version unchanged (identity). This is correct for npm, Go, Deno, plain, and most targets where semver is used directly.
Targets with different version conventions (e.g. PyPI's PEP 440) override this to translate from semver to the ecosystem format.
#publication_probe
def publication_probe(self, dir_path, version, ctx=None)Probe the registry to determine if a specific version is published.
Returns a PublicationProbeResult with one of three statuses: PUBLISHED: the version exists on the registry. UNPUBLISHED: the version does not exist on the registry. UNPROBEABLE: this target cannot probe (no API, no name, etc.).
The default implementation returns UNPROBEABLE. Targets with registry APIs (npm, pypi, go) override this to query the registry.
#cached_registry_probe
def cached_registry_probe(self, dir_path, version, ctx=None)Ask the REGISTRY ITSELF whether a version is out in the world.
A second probe, deliberately narrower than :meth:publication_probe. It exists because a target's primary probe does not have to ask the registry: Go's asks the git remote whether the version's tag exists, which is the right question for "did we tag this?" and the wrong one for "can anyone still download this?" -- proxy.golang.org caches a module version permanently the first time it is resolved, so a deleted tag reads as never-published while the proxy goes on serving it.
THE CONTRACT IS TWO-VALUED, not three: PUBLISHED, or UNPROBEABLE. This probe only ever ADDS positive evidence. A registry that indexes lazily is absent-by-default for a version nobody has fetched yet, so its silence must never be reported as UNPUBLISHED -- that would let registry lag clear a destructive operation.
The default returns UNPROBEABLE. The fact is supports_cached_registry_probe.
#dev_install_command
def dev_install_command(self, project_dir)Specs for local install via rlsbl dev install, keyed by mode.
Subclasses override to return spec dicts for the "global" and/or "venv" modes. See the protocol docstring for the spec format. Default returns {"global": None, "venv": None} (unsupported).
#supports_publication_probe
def supports_publication_probe(self)Whether this target can ask its registry if a version is published.
#supports_cached_registry_probe
def supports_cached_registry_probe(self)Whether this target has a registry-side probe beyond its primary one.
#supports_read_name
def supports_read_name(self)Whether this target can read a package name out of its manifest.
#supports_read_metadata
def supports_read_metadata(self)Whether this target can read license/description from its manifest.
#supports_dev_install
def supports_dev_install(self)Whether rlsbl dev install has anything to run for this target.
Behavioural rather than override-based: a subclass can inherit a dev_install_command whose specs resolve to nothing for it, and the honest answer there is "no".
Asked of :data:NOT_A_PROJECT_DIR, so the answer is the target's, not the current directory's -- see that constant for what asking "." used to do.
#provides_ci_templates
def provides_ci_templates(self)Whether this target ships a CI workflow template.
Answered from the template directory rather than declared: a target provides CI templates exactly when its template directory contains ci.yml.tpl, which is the file the scaffold renders into .github/workflows/ci.yml.
#_has_template
def _has_template(self, filename)Whether this target's template directory ships filename.
#supports_import_analysis
def supports_import_analysis(self)Whether rlsbl can read this target's sources to follow imports.
Derived from the target implementing find_dead_modules: the dead-module detectors and the workspace dependency checks (deps-unused and friends) both rest on the same import scanners, so a target that can answer one can answer the others.
#supports_circular_dep_analysis
def supports_circular_dep_analysis(self)Whether cycle detection is meaningful for this target's ecosystem.
Derived from the find_circular_dependencies override. Go deliberately does not implement it: the compiler already rejects circular imports, so a checker would only ever agree with it.
#find_dead_modules
def find_dead_modules(self, root, *, exclude_dirs=None, suppress=frozenset())Find source files or packages nothing else references.
Returns a list of (path, reason) pairs, where reason is the ecosystem-specific explanation shown to the user ("not imported by any other module", "not reachable from any entry point", ...). The default returns nothing: a target with no import scanner has no opinion.
Args:
root: project root to scan.exclude_dirs: sibling directories to keep out of the scan.suppress: declared exclusions (legitimate non-entry points),
threaded into the detector where the detector supports it so a listed file cannot keep other modules alive.
#find_circular_dependencies
def find_circular_dependencies(self, root, *, exclude_dirs=None)Find import cycles within this target's sources.
Returns a list of cycles, each a list of module identifiers. The default returns nothing.
#supports_version_query
def supports_version_query(self)Whether this target's registry can be asked for a latest version.
Derived from the query_latest_version override. rlsbl.targets.targets_with_version_queries() is the set form.
#supports_name_claim
def supports_name_claim(self)Whether rlsbl claim-name can reserve a name on this registry.
Derived from the claim_placeholder override. rlsbl.targets.claimable_targets() is the set form.
#supports_yank
def supports_yank(self)Whether this target's registry offers a removal action.
Derived from the yank override. The base answers UNSUPPORTED, so a target that does not override it has nothing to run.
#has_builtin_test_runner
def has_builtin_test_runner(self)Whether this target ships a built-in test runner.
Derived from the override rather than declared, so the answer cannot drift from the method. Callers that need the SET of such targets ask rlsbl.targets.targets_with_builtin_tests().
#run_tests
def run_tests(self, *, project_dir=None, workspace_root=None, skip_sync=False, config=None, check_timeout=None)Run this target's built-in test suite.
Targets whose ecosystem has a standard test command (uv run pytest, go test, npm test, the Gradle/Maven test task) override this. The default answers SKIPPED naming the target.
That default is the whole point of the method. The name chain this replaced ended in a bare return True, so a release of a project whose target has no runner recorded a PASSING test step for a suite that never ran.
Returns a :class:~.outcomes.SuiteRunOutcome.
#rewrite_mirror_identity
def rewrite_mirror_identity(self, clone_dir, mirror_remote)Rewrite this target's identity manifests to the MIRROR's identity.
Called inside the mirror clone, before the scaffold commit is made, for every target that declares :attr:mirror_identity_files. Returns the repository-relative paths it rewrote (empty when nothing needed changing), and raises when the mirror's identity cannot be derived -- never silently leaves a manifest naming the monorepo, which is a manifest that does not resolve from the mirror.
The default does nothing, which is right for every target whose manifest names no repository.
#yank
def yank(self, project_dir, version, tag, *, reason=None, dry_run=False)Remove a published version from this target's registry.
Targets whose registry offers a removal action (npm's deprecate, Go's retract directive, PyPI's manual yank) override this. The default answers UNSUPPORTED naming the target, so rlsbl release yank reports a target it cannot act on instead of passing over it.
Returns a :class:~.outcomes.YankOutcome.