Skip to content
Release targets
On this page

The rlsbl release targets including npm, PyPI, Go, Docker and Flutter, with auto-detection, the ReleaseTarget protocol, and per-axis support properties.

#Release targets

Each release target handles version reading, writing, and tag formatting for a specific ecosystem. Targets do not handle publishing — that is the responsibility of pipelines. The table below is the enumeration:

Release targets
NameEcosystemDetection filesVersion fileAuto-detectableTag formatMonorepo tag formatread_nameread_metadataci_templatesdev_install
dartDart / pub.devpubspec.yamlpubspec.yamlyesv{version}{name}@v{version}
denoDeno / JSRdeno.json, deno.jsoncdeno.jsonyesv{version}{name}@v{version}global: deno install, venv: deno cache .
dockerDockerDockerfileVERSIONyesv{version}{name}@v{version}
flutterFlutterpubspec.yaml (flutter)pubspec.yamlyesv{version}{name}@v{version}
goGo modulesgo.modVERSIONyesv{version}{path}/v{version}global: go install
hexElixir / Hexmix.exsmix.exsyesv{version}{name}@v{version}global: mix deps.get, venv: mix deps.get
mavenJava / Mavenbuild.gradle.kts, build.gradle, pom.xml---yesv{version}{name}@v{version}
native-androidAndroid---build.gradleyesv{version}{name}@v{version}
native-iosiOS------yesv{version}{name}@v{version}
npmNode.js / npmpackage.jsonpackage.jsonyesv{version}{name}@v{version}global: npm link, venv: npm install
pgdesignPostgreSQLpgdesign.tomlpgdesign.tomlyesv{version}{name}@v{version}
plainPlainVERSION (conditional)VERSIONconditionalv{version}{name}@v{version}
pypiPython / PyPIpyproject.tomlpyproject.tomlyesv{version}{name}@v{version}global: uv tool install -e ., venv: uv sync --all-packages
specSpecificationversion.jsonversion.jsonyesspec-v{version}{name}@v{version}
swiftSwift (SPM)Package.swiftVERSIONyesv{version}{name}@v{version}global: swift build
swift-appleSwift (Apple)---VERSIONnov{version}{name}@v{version}
zigZigbuild.zig.zon, build.zigVERSIONyesv{version}{name}@v{version}global: zig build install

All targets share core release functionality: version bumping, git tagging, and GitHub Release creation. The table above shows the optional operations that vary by ecosystem, each derived from the target rather than declared beside it.

#Target vs Pipeline

Targets and pipelines serve orthogonal purposes in the release flow. Targets handle versioning (reading and writing version strings in manifest files), while pipelines handle publishing (uploading artifacts to registries). This separation allows flexible combinations where the versioning ecosystem differs from the publish destination:

Target vs Pipeline
ConcernTargetsPipelines
What they doRead/write versions in manifest filesPublish artifacts to registries
Configured inAuto-detected or targets array in config.jsonpipelines dict in config.json
When they runVersion bump step of rlsbl release runPublish step (CI or local)
Examplepypi target writes to pyproject.tomlpypi pipeline publishes via OIDC

A project can have a target for versioning without a corresponding pipeline (e.g., Go libraries that need no publish step), or a pipeline type that differs from the target (e.g., an npm target with a cloudflare-pages pipeline for deployment).

#Auto-detection

When rlsbl release run, rlsbl scaffold, or rlsbl targets needs to know which targets apply, it calls detect_targets(dir_path) which scans the project directory for manifest files and applies content-based disambiguation when multiple targets could match. The detection logic follows two paths:

  1. Explicit configuration — If .rlsbl/config.json contains a targets array, that list is authoritative. Each entry is either a string ("npm") or a dict with name and optional path (for subdirectory targets). Unknown target names are warned and skipped.
  1. Auto-detection fallback — If no targets array exists in config, every registered target's detect() method is called against the directory. Targets that return True are included.

The auto_detectable ClassVar on each target controls detection behavior. Which targets hold which value is the Auto-detectable column of the table above, not a list repeated here:

Auto-detection
ValueMeaning
"yes"Standard file-based detection
"conditional"Detects only when specific conditions are met beyond file presence (plain requires a VERSION file AND no other manifest)
"no"Never auto-detected; must be declared in config

#Detection priority

When multiple targets could match the same manifest file (e.g., a project with both pubspec.yaml and a flutter: section, or build.gradle matching both native-android and maven), targets use content-based checks to disambiguate and ensure exactly one target claims each project:

  • dart excludes projects where pubspec.yaml contains a flutter: key
  • flutter requires pubspec.yaml with a flutter: key
  • plain yields to any other target's manifest file
  • native-android vs maven: both use build.gradle.kts/build.gradle, but native-android checks for com.android.application plugin declaration

#Detection files

Each target class declares a detection_files ClassVar listing the filenames whose presence triggers detection; the Detection files column of the table above is that declaration, rendered. These filenames are aggregated into the PROJECT_MANIFESTS set used by workspace-level checks to detect unregistered projects in a monorepo. A target whose column is blank either decides by file content (native-android shares the Gradle files with maven and inspects them; native-ios scans for an .xcodeproj) or never auto-detects at all (swift-apple is selected only by explicit declaration). Shared manifests are disambiguated by content and annotated in the rendered column — flutter and dart share pubspec.yaml, and plain yields to every other target's manifest.

#The ReleaseTarget protocol

Every target implements a runtime-checkable Protocol that defines the interface for version management, detection, tag formatting, and CI template generation. Each target provides concrete implementations for its ecosystem's conventions. The key methods:

The ReleaseTarget protocol
MethodPurpose
detect(dir_path) -> boolCheck if this target applies to a directory
read_version(dir_path) -> strRead the current version string from the manifest
write_version(dir_path, version, ctx) -> list[str]Write a new version; returns list of modified file paths (relative to dir_path)
version_file(dir_path) -> str or NoneFilename that holds the version (e.g., "package.json")
read_name(dir_path, ctx) -> str or NoneRead the project/package name from the manifest
read_metadata(dir_path) -> dictRead optional metadata (license, description)
tag_format(version) -> strFormat the git tag (default: v{version})
monorepo_tag_format(name, version, path) -> strFormat monorepo git tag (default: {name}@v{version})
monorepo_tag_glob(name, path) -> strGlob pattern matching all monorepo version tags
template_vars(dir_path, ctx) -> dictExtract template variables for CI generation
template_mappings(ctx) -> list[dict]Target-specific template-to-output-path mappings
dev_install_command(project_dir) -> dictReturn install specs for rlsbl dev install
build(dir_path, version) -> NonePre-publish build step (no-op by default)

#rlsbl.targets.protocol

Release target protocol defining the formal interface that all target implementations must satisfy for detection, versioning, and scaffolding.

#ReleaseTarget

Protocol defining a release target.

Targets handle version management, scaffolding templates, and optionally build/publish steps for a specific ecosystem (npm, pypi, go, codehome, docs, etc.)

#name

python
def name(self) -> str

Unique identifier for this target (e.g. 'npm', 'pypi', 'codehome').

#supports_publication_probe

python
def supports_publication_probe(self) -> bool

Whether publication_probe gives a real answer for this target.

#supports_cached_registry_probe

python
def supports_cached_registry_probe(self) -> bool

Whether cached_registry_probe gives a real answer for this target.

#release_materialization_policy

python
def release_materialization_policy(self) -> str

Whether a reconcile may recreate this target's missing release refs.

"materialize" -- recreating a released version's absent ref is a pure repair. "refuse-identity-transition" -- the target's tags ARE its published artifact, so a version whose published identity has since changed must not have its refs recreated under the new one.

#supports_read_name

python
def supports_read_name(self) -> bool

Whether read_name reads a real name for this target.

#supports_read_metadata

python
def supports_read_metadata(self) -> bool

Whether read_metadata reads real metadata for this target.

#supports_dev_install

python
def supports_dev_install(self) -> bool

Whether dev_install_command yields a spec for any mode.

#provides_ci_templates

python
def provides_ci_templates(self) -> bool

Whether this target ships a CI workflow template to scaffold.

#supports_import_analysis

python
def supports_import_analysis(self) -> bool

Whether rlsbl can read this target's sources to follow imports.

#supports_circular_dep_analysis

python
def supports_circular_dep_analysis(self) -> bool

Whether cycle detection is meaningful for this target's ecosystem.

#has_builtin_test_runner

python
def has_builtin_test_runner(self) -> bool

Whether this target ships a built-in test runner.

#supports_version_query

python
def supports_version_query(self) -> bool

Whether this target's registry answers a latest-version query.

#supports_name_claim

python
def supports_name_claim(self) -> bool

Whether a name can be claimed on this target's registry.

#supports_yank

python
def supports_yank(self) -> bool

Whether this target's registry offers a removal action.

#detect

python
def detect(self, dir_path: str) -> bool

Check if this target is present/applicable in the given directory.

#read_version

python
def read_version(self, dir_path: str) -> str

Read the current version from the target's manifest file.

#read_name

python
def read_name(self, dir_path: str, ctx) -> str | None

Read the project's package name from the manifest, or None.

#read_metadata

python
def read_metadata(self, dir_path: str) -> dict[str, str]

Read optional metadata (license, description) from the manifest.

#write_version

python
def write_version(self, dir_path: str, version: str, ctx) -> None

Write a new version to the target's manifest file (atomic).

#version_file

python
def version_file(self, dir_path: str | None=None) -> str | None

Filename that holds the version (e.g. 'package.json'), or None if inherited.

When dir_path is provided, implementations may resolve the filename dynamically (e.g. Deno choosing between deno.json and deno.jsonc).

#tag_format

python
def tag_format(self, version: str) -> str

Format the git tag for a release. Returns f'v{version}' by default.

#monorepo_tag_format

python
def monorepo_tag_format(self, name: str, version: str, path: str | None=None) -> str

Format the git tag for a monorepo release. Default: f'{name}@v{version}'.

#monorepo_tag_glob

python
def monorepo_tag_glob(self, name: str, path: str | None=None) -> str

Return a glob pattern matching all monorepo version tags. Default: f'{name}@v*'.

#companion_tags

python
def companion_tags(self, name: str, version: str, path: str | None=None) -> list[str]

Return additional tags to create alongside the primary release tag.

Ecosystems that require extra tags (e.g. Go module proxy tags) override this. The default returns no companion tags.

#expected_refs

python
def expected_refs(self, version: str, context)

Every git ref version owns: the primary tag, companions, aliases.

THE single authority for a released version's ref set. context is a :class:~rlsbl.targets.refs.RefContext; the return is an :class:~rlsbl.targets.refs.ExpectedRefs. The release flow creates and pushes exactly this set and the unpublished-refs check renders exactly this set against reality, so the two can never diverge.

Composed from the per-target facts above rather than overridden: no target implements this itself.

#format_version

python
def format_version(self, version: str) -> str

Translate a semver version into this ecosystem's version format.

The default is the identity, which is correct for npm, Go, Deno and most others. PyPI overrides it for PEP 440.

#normalize_package_name

python
def normalize_package_name(self, raw_name: str) -> str

Reduce a package name to the form this registry compares by.

PyPI folds runs of -_. to a single hyphen, npm removes them, Go compares the last path segment. The default lowercases.

#query_latest_version

python
def query_latest_version(self, name: str) -> dict

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 default answers error naming the target, so "this ecosystem has no version API" is never mistaken for "the package is unpublished".

#claim_placeholder

python
def claim_placeholder(self, name: str, tmpdir: str) -> str

Publish a minimal placeholder package to reserve name.

Targets whose registry accepts a publish override this; the default raises, and claimable_targets() derives the accepted set from exactly this method.

#registry_display_name

python
def registry_display_name(self) -> str

How to spell this target's registry in user-facing output.

#template_dir

python
def template_dir(self) -> str | None

Absolute path to target-specific template directory, or None.

#shared_template_dir

python
def shared_template_dir(self) -> str | None

Absolute path to shared template directory, or None.

#template_vars

python
def template_vars(self, dir_path: str, ctx) -> dict[str, str]

Extract template placeholder values from the project.

#template_mappings

python
def template_mappings(self, ctx) -> list[dict[str, str]]

Target-specific template-to-output-path mappings.

#shared_template_mappings

python
def shared_template_mappings(self, ctx) -> list[dict[str, str]]

Shared template-to-output-path mappings.

#check_project_exists

python
def check_project_exists(self, dir_path: str) -> bool

Check if the target's project file exists (alias for detect).

#get_project_init_hint

python
def get_project_init_hint(self) -> str

Human-readable hint for initializing a project for this target.

#publication_probe

python
def publication_probe(self, dir_path: str, version: str, ctx=None)

Probe the registry to determine if a specific version is published.

Returns a PublicationProbeResult (PUBLISHED, UNPUBLISHED, or UNPROBEABLE). Default: UNPROBEABLE.

#cached_registry_probe

python
def cached_registry_probe(self, dir_path: str, version: str, ctx=None)

Ask the REGISTRY ITSELF whether a version is out in the world.

The second probe, for a target whose primary one answers from somewhere other than the registry. Two-valued: PUBLISHED or UNPROBEABLE, never UNPUBLISHED -- a lazily-indexed registry's silence is not evidence. Default: UNPROBEABLE. The fact is supports_cached_registry_probe.

#build

python
def build(self, dir_path: str, version: str, *, config: dict | None=None) -> None

Pre-publish build step (e.g. generate docs). No-op by default.

#dev_install_command

python
def dev_install_command(self, project_dir: str) -> dict[str, dict | None]

Return the local-install specs for this target, keyed by mode.

Used by rlsbl dev install to install/uninstall the project for local development. Returns a dict with two keys:

"global": spec for the global-install mode, where the project is installed as a globally-available tool or symlink (e.g. uv tool install -e ., npm link, go install). None if the target has no global-install concept.

"venv": spec for the local/venv-install mode, where dependencies are fetched into the project's own environment without exposing a global CLI (e.g. uv sync, npm install). None for targets that have no separate local-environment concept (e.g. Go, Zig, Swift).

Each spec dict has the shape: { "tool": "uv", "args": ["tool", "install", "-e", "."], "uninstall_args_template": ["tool", "uninstall", "{name}"], "purpose": "for editable Python install", }

Fields: tool: CLI tool that must be on PATH. args: argv passed to the tool to install. uninstall_args_template: argv list of templates passed to the tool to uninstall. Each entry may contain {name} (replaced with the project's package name) or {dir} (replaced with the project directory basename). None means uninstall is not supported for this mode of this target. purpose: human-readable string for the require_tool error message.

The returned dict may also carry an optional top-level "reason" key: a human-readable explanation for why the modes are None (e.g. "Go library: nothing to install"). rlsbl dev install surfaces it in the skip message instead of the generic "not yet supported" line.

#find_dead_modules

python
def find_dead_modules(self, root: str, *, exclude_dirs=None, suppress=frozenset()) -> list[tuple[str, str]]

Find source files or packages nothing else references.

Returns (path, reason) pairs, where reason is the ecosystem-specific explanation shown to the user. The default returns nothing: a target with no import scanner has no opinion, and supports_import_analysis is derived from this override.

#find_circular_dependencies

python
def find_circular_dependencies(self, root: str, *, exclude_dirs=None) -> list[list[str]]

Find import cycles within this target's sources.

Returns a list of cycles, each a list of module identifiers. The default returns nothing, and supports_circular_dep_analysis is derived from this override.

#run_tests

python
def run_tests(self, *, project_dir: str | None=None, workspace_root: str | None=None, skip_sync: bool=False, config: dict | None=None, check_timeout: int | None=None)

Run this target's built-in test suite.

Returns a SuiteRunOutcome. The default answers SKIPPED naming the target, so a project whose target ships no runner records a visible skip rather than a passing step for a suite that never ran.

#rewrite_mirror_identity

python
def rewrite_mirror_identity(self, clone_dir: str, mirror_remote: str) -> list

Rewrite mirror_identity_files onto the mirror's own identity.

Returns the repository-relative paths rewritten. Raises when the mirror's identity cannot be derived: a manifest still naming the monorepo is one that does not resolve from the mirror.

#yank

python
def yank(self, project_dir: str, version: str, tag: str, *, reason: str | None=None, dry_run: bool=False)

Remove a published version from this target's registry.

Returns a YankOutcome. The default answers UNSUPPORTED naming the target, so rlsbl release yank reports a target it cannot act on instead of passing over it.

#BaseTarget defaults

All concrete targets extend BaseTarget, which provides sensible defaults for common operations so that individual targets only need to override ecosystem-specific behavior. The base class handles tag formatting, shared template mappings, and stub implementations for optional methods:

  • Tag format: v{version} (standalone) / {name}@v{version} (monorepo)
  • Shared template mappings: CHANGELOG.md, .gitignore, hooks, lint configs, unreleased.jsonl
  • No-op stubs for build(), dev_install_command(), read_name(), read_metadata()
  • check_project_exists() delegates to detect()

Individual targets override only the methods specific to their ecosystem.

#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

python
def name(self)

Target registry name. Subclasses must override.

#detect

python
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

python
def version_file(self, dir_path=None)

Return the relative path of the file that holds the project version.

#tag_format

python
def tag_format(self, version)

Return the git tag string for a standalone release version.

#monorepo_tag_format

python
def monorepo_tag_format(self, name, version, path=None)

Return the git tag string for a monorepo package release.

#monorepo_tag_glob

python
def monorepo_tag_glob(self, name, path=None)

Return a glob pattern matching all version tags for a monorepo package.

#template_dir

python
def template_dir(self)

Return the path to this target's ecosystem-specific template directory.

#shared_template_dir

python
def shared_template_dir(self)

Return the path to the shared template directory common to all targets.

#read_name

python
def read_name(self, dir_path, ctx)

Read the project name from the target's manifest file.

#read_metadata

python
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

python
def template_vars(self, dir_path, ctx)

Return template variables extracted from the project for scaffold rendering.

#template_mappings

python
def template_mappings(self, ctx)

Return the list of target-specific template-to-file mappings for scaffolding.

#shared_template_mappings

python
def shared_template_mappings(self, ctx)

Return template-to-file mappings shared across all targets.

#_lint_config_mappings

python
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

python
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

python
def check_project_exists(self, dir_path)

Return True if the project's manifest file exists in dir_path.

#get_project_init_hint

python
def get_project_init_hint(self)

Return a user-facing hint for initializing a project of this target type.

#write_version

python
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

python
def _resolve_build_timeout(self, config)

Resolve the build timeout from config, then the shipped default.

  1. config["build_timeout"] -- an int, or a dict keyed by target

name with an optional "default" entry

  1. self.BUILD_TIMEOUT_DEFAULT class variable

There is deliberately no environment-variable layer: build budgets are declared in .rlsbl/config.json, never picked up from the ambient environment.

#build

python
def build(self, dir_path, version, *, config=None)

Build distributable artifacts for this target. No-op by default.

#companion_tags

python
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 (without v prefix).
  • 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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
def supports_publication_probe(self)

Whether this target can ask its registry if a version is published.

#supports_cached_registry_probe

python
def supports_cached_registry_probe(self)

Whether this target has a registry-side probe beyond its primary one.

#supports_read_name

python
def supports_read_name(self)

Whether this target can read a package name out of its manifest.

#supports_read_metadata

python
def supports_read_metadata(self)

Whether this target can read license/description from its manifest.

#supports_dev_install

python
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

python
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

python
def _has_template(self, filename)

Whether this target's template directory ships filename.

#supports_import_analysis

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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.

#What a target supports

There is no declared capability set. What a target supports is derived from the target itself, one property per axis, so the answer cannot disagree with the code that implements it. The axes are declared once, in rlsbl.targets.introspect, and every registered target must answer every one of them — a target that cannot, or an axis added to the protocol without a declaration here, is an error at import time.

Completeness is stated by exclusion, so no naming convention can hide a fact: every public attribute of BaseTarget must be either the source of an axis or listed in NON_AXIS_ATTRIBUTES with the one line saying why it is not a per-target fact (almost all of them are operations — build, read_name, yank — whose "can this target do it at all?" is itself an axis). An unclassified attribute, and an exclusion naming an attribute that no longer exists, are both errors at import time:

What a target supports
AxisWhat it says about a target
ecosystemHuman-readable name of the registry or platform, rendered in the docs support matrix.
auto_detectableWhether detection runs without configuration: yes, no, or conditional.
detection_filesFilenames whose presence in a directory declares this target.
content_based_detectionWhether detection inspects file content (the target overrides detect).
version_fileFile that holds the version, or null when the filename is per-project and cannot be stated statically.
tag_formatStandalone release tag pattern.
monorepo_tag_formatMonorepo release tag pattern.
monorepo_tag_globGlob matching every monorepo version tag for a package.
companion_tagsExtra tags created alongside the primary release tag.
format_versionHow the ecosystem spells the semver version 1.2.3-rc.1.
registry_display_nameHow this target's registry is spelled in user-facing output.
build_timeout_defaultSeconds allowed for this target's build before it is a timeout.
project_init_hintWhat a user is told to run to create a project of this target.
publisher_binds_to_repositoryWhether publishing is authorized for a REPOSITORY rather than for the package, so moving the code requires re-authorizing.
publisher_setup_urlWhere a repository-bound publisher is registered; empty when none is.
consumed_by_repository_urlWhether consumers resolve this target by repository URL and git tag rather than from a registry, so a monorepo member of this kind requires a standalone mirror to be consumable at all.
mirror_identity_filesManifests naming the repository the package lives in, which the mirror's scaffold commit rewrites to the mirror's own identity.
release_materialization_policyWhether a reconcile may recreate a released version's missing refs unconditionally, or must refuse when a recorded identity transition puts that version under a different published identity.
supports_read_nameReads a package name from its manifest (overrides read_name).
supports_read_metadataReads license and description from its manifest (overrides read_metadata).
supports_publication_probeCan ask its registry whether a version is published.
supports_cached_registry_probeHas a second, registry-side publication probe, because its primary one answers from somewhere other than the registry.
supports_version_queryIts registry answers a latest-version query.
supports_name_claimA name can be claimed on its registry by publishing a placeholder.
claim_token_env_varsEnvironment variables, any one of which authenticates a name claim.
supports_yankIts registry offers a removal action for a published version.
provides_ci_templatesShips ci.yml.tpl, so scaffold can generate a CI workflow.
supports_dev_installrlsbl dev install has something to run for this target.
dev_install_commandThe local-install specs, keyed by mode (global, venv).
supports_import_analysisrlsbl can read its sources to follow imports (overrides find_dead_modules).
supports_circular_dep_analysisImport-cycle detection is meaningful for this ecosystem.
supports_dep_floorsIts manifest states dependency floors a lockfile can resolve ahead of.
lint_languageWhich library-lint language its sources are written in, or null.
has_builtin_test_runnerShips a built-in test runner (overrides run_tests).
shares_workspace_environmentWorkspace members of this target resolve into ONE shared environment.

Every target's answer to every axis is generated into rlsbl/data/support-matrix.json, which is committed, rendered into the tables on this page, and kept in step with the code by the target-matrix-fresh check.

Each axis is consulted at the point of use. rlsbl dev install asks each target for its install specs and skips the ones that have none, naming them. Four sites decide whether to run a publication probe, and every one of them reads supports_publication_probe rather than defaulting the answer:

What a target supports
SiteWhat it decides
Each pipeline's pre-publish checkwhether to skip a version the registry already serves
The release's post-publish verificationwhich targets belong in the verified set
The undo evidence layer (rlsbl release undo --version)whether a target can contribute registry evidence
rlsbl release yankeach target's publication status before removal

The name-consistency check is not one of these: it asks every detected target for a name and compares the ones that answer, reporting the targets that returned nothing alongside the result rather than skipping them silently.

#Ecosystem classification

Each target declares an ecosystem string: the human-readable name of the registry or platform it publishes to. It is one of the target protocol's axes, so every target answers it, and the answers are what the Ecosystem column of the table above renders — that table is generated from the committed support matrix, not hand-maintained. No command prints the label: rlsbl targets names the detected targets, and rlsbl monorepo list names each member's releasable and flags.

#Per-target notes

#npm

  • Reads/writes package.json
  • Detects package manager by walking up to git root looking for lock files: pnpm-lock.yaml (pnpm), yarn.lock (yarn), package-lock.json (npm), falls back to npm
  • Package manager choice affects CI template selection (separate templates for pnpm and yarn)
  • Extracts binCommand, repoName, registryUrl, publishSetup template variables
  • dev_install: global via npm link, local via npm install

#pypi

  • Reads/writes pyproject.toml (via tomlkit for comment preservation)
  • Also bumps __version__ in {pkg_name}/__init__.py or src/{pkg_name}/__init__.py if present
  • Build step handles monorepo path dependency rewriting (copies to temp dir, rewrites pyproject.toml, builds there)
  • dev_install: global via uv tool install -e ., local via uv sync

#go

  • Detection: go.mod presence
  • Version stored in VERSION file (not go.mod — Go modules have no version field in go.mod)
  • Monorepo tag format uses path prefix: {path}/v{version} (Go module proxy convention)
  • Detects library vs binary projects via go list (any package main package, regardless of file names or layout)
  • GoReleaser integration for binary projects; library projects need no publish step. Ambiguous multi-main layouts require install_paths on the go pipeline config.
  • npm binary wrapper support, activated with {"npm_wrapper": {"enabled": true}} in .rlsbl/config.json. Per-platform packages publish under bare suffixed names (<bin>-linux-x64, <bin>-darwin-arm64, ...) plus a meta wrapper named <bin>. Scoped npm names (@scope/name) are banned by ecosystem policy; each bare per-platform name must be independently approved (rlsbl check-name) like any other package name. A stale npm_wrapper.scope/npm_scope key is a hard error.
  • Homebrew tap support via homebrew config
  • dev_install: go install <install_paths> from the go pipeline config (no venv concept); undeclared install_paths on a module that has main packages is a hard error from rlsbl dev install, which is the command that has a project directory and something to install from it. Nothing that merely enumerates targets — the support axes, the derived help counts — ever hands the target a project directory, so no other command can reach that refusal

#deno

  • Handles both deno.json and deno.jsonc (prefers .json when both exist)
  • For .jsonc files, uses regex-based version replacement to preserve comments
  • For .json files, uses standard JSON rewrite preserving indent
  • version_file() resolves dynamically based on which config file exists

#dart

  • Reads/writes pubspec.yaml using ruamel.yaml for comment preservation
  • Strips build number suffix (+N) when reading, handles it when writing
  • Build number strategy configurable via build_number.enabled and build_number.strategy in config
  • Excludes projects with flutter: key (those belong to the flutter target)

#flutter

  • Extends DartTarget (inheritance, not duplication)
  • Detection: pubspec.yaml must contain a flutter: key
  • Inherits all dart version read/write logic including build number handling
  • No detection_files of its own (shares pubspec.yaml with dart)

#swift

  • Detection: Package.swift presence
  • Version stored in VERSION file
  • dev_install: global via swift build, no venv concept

#swift-apple

  • Extends SwiftTarget (inheritance)
  • Never auto-detected (auto_detectable = "no") — must be declared in .rlsbl/config.json targets array
  • Provides macOS-only CI templates (uses macos-latest runners instead of ubuntu-latest)
  • No dev_install support

#zig

  • Detection: build.zig.zon or build.zig
  • Version stored in VERSION file with automatic build.zig.zon synchronization
  • npm binary wrapper support for cross-compiled binaries, activated with {"npm_wrapper": {"enabled": true}}. Publishes bare per-platform names (<bin>-linux-x64, ...) plus a meta wrapper named <bin>; scoped names are banned and each name needs explicit rlsbl check-name approval. A stale npm_wrapper.scope/npm_scope key is a hard error.
  • Cross-compilation target map for 6 platforms (linux/darwin/win32, x64/arm64)

#docker

  • Detection: Dockerfile presence
  • Version stored in VERSION file
  • Image name derived from config (docker.image) or directory name

#maven

  • Detection: build.gradle.kts, build.gradle, or pom.xml
  • Supports three build systems: Maven (pom.xml), Gradle (build.gradle), Gradle Kotlin DSL (build.gradle.kts)

#spec

  • Detection: version.json file presence
  • Version: reads/writes {"version": "X.Y.Z"} in version.json
  • Its CI template is a stub, for users to add their own validation commands; there is no publish step
  • Use case: spec-only projects that need version tracking without any build or publish step — the tagged GitHub Release is the publication

#pgdesign

  • Detection: pgdesign.toml file presence
  • Version: reads/writes the version field in pgdesign.toml
  • No publish mechanism — version bumping only (the tagged GitHub Release is the artifact)
  • Use case: PostgreSQL schema design projects managed by the pgdesign tool

#native-ios

  • Content-based detection: scans for .xcodeproj/project.pbxproj with MARKETING_VERSION
  • Also supports Tuist Project.swift
  • See native-targets.md for details

#native-android

  • Content-based detection: checks build.gradle/build.gradle.kts for com.android.application plugin
  • Manages both versionName (semver) and versionCode (integer, auto-incremented)
  • See native-targets.md for details

#plain

  • Detection: conditional — VERSION file must exist AND no other target manifest is present
  • Version: reads/writes plain text VERSION file (single line, e.g. 0.5.2)
  • Supports nothing beyond version bumping and tagging — its row in the table above is blank on every optional axis, so scaffold generates no CI workflow, rlsbl dev install has nothing to run, and no pipeline links to it
  • The stand-off set: plain will not auto-detect when any other target's manifest is present. That set is derived from every registered target's detection_files, plus Cargo.toml and selfdoc.json — manifests left behind by retired targets that no current target claims
  • Use case: projects that need version tracking but don't fit any ecosystem (e.g., documentation-only repos, script collections, infrastructure projects)
  • Also bumps pyproject.toml version if that file exists with a [project].version field

#Check support matrix

Some checks are universal (they run for any target), while others only apply to targets with language-specific import scanners or AST analysis. This matrix shows which target-specific checks support which targets.

Check support matrix
Checkpypigonpmdarthexswiftswift-applemavenflutter
ci-publish-secretsnonoyesnoyesnonoyesno
circular-depsyesn/ayesyesnononoyesyes
cross-repo-path-sourcesyesnononononononono
dead-modulesyesyesyesyesnononoyesyes
dead-modules-staleyesyesyesyesnononoyesyes
dep-floorsyesyesyesnononononono
dep-locksyesyesyesnononononono
deps-dev-in-libyesyesyesyesnononoyesyes
deps-runtime-test-onlyyesyesyesyesnononoyesyes
deps-undeclaredyesyesyesyesnononoyesyes
deps-unusedyesyesyesyesnononoyesyes
dev-overlay-driftyesnononononononono
dunder-version-missingyesnononononononono
formatyesnononononononono
format-scope-guardyesnononononononono
go-module-identitynoyesnonononononono
ldflags-symbolnoyesnonononononono
library-lintyesyesyesnonononoyesno
lintyesnononononononono
lint-scope-guardyesnononononononono
maven-central-metadatanononononononoyesno
mirror-requirednononononoyesyesnono
npm-private-mismatchnonoyesnononononono
ruff-lintyesnononononononono
strictspec-generated-flooryesnononononononono
test-suiteyesyesyesnonononoyesno
type-checkyesnononononononono
type-check-scope-guardyesnononononononono

All checks not listed here are universal and run for every target.

#Target implementations

The base target class defines the shared interface for version reading, version writing, detection, and version file location. Every concrete target implementation inherits from this base and override the methods relevant to their ecosystem's versioning conventions.

#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

python
def name(self)

Target registry name. Subclasses must override.

#detect

python
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

python
def version_file(self, dir_path=None)

Return the relative path of the file that holds the project version.

#tag_format

python
def tag_format(self, version)

Return the git tag string for a standalone release version.

#monorepo_tag_format

python
def monorepo_tag_format(self, name, version, path=None)

Return the git tag string for a monorepo package release.

#monorepo_tag_glob

python
def monorepo_tag_glob(self, name, path=None)

Return a glob pattern matching all version tags for a monorepo package.

#template_dir

python
def template_dir(self)

Return the path to this target's ecosystem-specific template directory.

#shared_template_dir

python
def shared_template_dir(self)

Return the path to the shared template directory common to all targets.

#read_name

python
def read_name(self, dir_path, ctx)

Read the project name from the target's manifest file.

#read_metadata

python
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

python
def template_vars(self, dir_path, ctx)

Return template variables extracted from the project for scaffold rendering.

#template_mappings

python
def template_mappings(self, ctx)

Return the list of target-specific template-to-file mappings for scaffolding.

#shared_template_mappings

python
def shared_template_mappings(self, ctx)

Return template-to-file mappings shared across all targets.

#_lint_config_mappings

python
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

python
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

python
def check_project_exists(self, dir_path)

Return True if the project's manifest file exists in dir_path.

#get_project_init_hint

python
def get_project_init_hint(self)

Return a user-facing hint for initializing a project of this target type.

#write_version

python
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

python
def _resolve_build_timeout(self, config)

Resolve the build timeout from config, then the shipped default.

  1. config["build_timeout"] -- an int, or a dict keyed by target

name with an optional "default" entry

  1. self.BUILD_TIMEOUT_DEFAULT class variable

There is deliberately no environment-variable layer: build budgets are declared in .rlsbl/config.json, never picked up from the ambient environment.

#build

python
def build(self, dir_path, version, *, config=None)

Build distributable artifacts for this target. No-op by default.

#companion_tags

python
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 (without v prefix).
  • 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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
def supports_publication_probe(self)

Whether this target can ask its registry if a version is published.

#supports_cached_registry_probe

python
def supports_cached_registry_probe(self)

Whether this target has a registry-side probe beyond its primary one.

#supports_read_name

python
def supports_read_name(self)

Whether this target can read a package name out of its manifest.

#supports_read_metadata

python
def supports_read_metadata(self)

Whether this target can read license/description from its manifest.

#supports_dev_install

python
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

python
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

python
def _has_template(self, filename)

Whether this target's template directory ships filename.

#supports_import_analysis

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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.

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • claudewheel A TUI Claude Code Launcher that lets you have more than one profile, manage sessions lifecycle, pick the exact CC version, model to use (even older unlisted ones), pick which GitHub account to use, etc.
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • go-toml-edit Zero-dep TOML editing library for Go with comment preservation
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • safegit git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • selfdoc Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search