On this page
Pipeline architecture: 9 built-in types, 3 auth patterns, custom assets, capability gating, launcher shims with download modes, and migration guide.
#Pipelines
#Overview
Pipelines handle publishing — where and how a release is distributed. They are configured in .rlsbl/config.json under the pipelines key, which supports 9 built-in pipeline types across 3 authentication patterns (token, credential, and unauthenticated). Each pipeline entry has a user-chosen name and specifies its type, auth mechanism, and optional asset configuration.
Pipelines are distinct from targets: targets determine which files get version-bumped (auto-detected from manifests), while pipelines determine where the release artifact is published (explicitly configured). A project can have an npm target for versioning but a cloudflare-pages pipeline for publishing, or multiple pipelines publishing to different registries.
#Targets vs pipelines
| Concern | Targets | Pipelines |
|---|---|---|
| Purpose | Version bumping | Publishing |
| Discovery | Auto-detected from manifests | Explicitly configured |
| Config location | Auto or targets array in config.json | pipelines object in config.json |
| Cardinality | One per ecosystem per project | Any number, user-named |
| Example | npm target bumps package.json version | npm pipeline runs npm publish in CI |
A project with no pipelines configured simply does not publish anywhere — version bumps, tags, and GitHub Releases still happen via targets.
#Publishing nowhere
pipelines is always a map of pipeline name to pipeline config. There is no scalar form: "pipelines": "none" is a config error, not a way to opt out. Two shapes express "publish nowhere", and they mean different things:
| Shape | Meaning |
|---|---|
"pipelines": {} | The project releases (version bump, tag, GitHub Release) but publishes to no registry. |
"publish_mode": "none" | Publishing is suppressed entirely — no publish workflow is scaffolded at all. See configuration. |
Any other non-map value is rejected with a ConfigError naming both shapes, at config-check time and again in the release preflight — before the release mutates anything.
#Configuration
Pipelines are configured in .rlsbl/config.json under the pipelines key. Each entry is keyed by a user-chosen name (any valid JSON string) and requires a type field (one of 9 built-in types), a local boolean field indicating whether publishing happens on the developer machine or in CI, and a target link naming the release target it publishes for (or null for a targetless publisher):
{
"pipelines": {
"my-pipeline-name": {
"type": "npm",
"local": false,
"target": "npm"
}
}
}#Fields
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | One of the 9 built-in pipeline types (see table below) |
local | bool | Yes | Whether to publish from the developer machine. false means CI handles it. |
target | string or null | Yes | The release target this pipeline publishes for. Must name an entry in the config's targets list, or be null for a targetless publisher (e.g. a docs deploy). There is no name-based inference. |
artifact | string | Yes (type go) | binary or library. Selects the go publish workflow. No default. See go. |
token_var | string | No | Env var name for the publish token. Each type has a default. |
username_var | string | No | Env var for username auth (docker only). |
password_var | string | No | Env var for password auth (docker only). |
assets | bool | No | Enable building and uploading target-specific artifacts to GitHub Releases. |
max_asset_size_mb | int | When assets or custom_assets is set | Maximum artifact size in MB. Release fails if any artifact exceeds this. |
custom_assets | array | No | List of custom build artifacts. Each entry: {name, build}. |
#Pipeline types
There are 9 built-in pipeline types covering all major package registries and deployment platforms. Each type implements ecosystem-specific authentication, build commands, and publish logic while sharing the common BasePipeline interface for custom assets and lifecycle hooks.
| Type | Auth method | Required env vars | Ecosystem |
|---|---|---|---|
| cloudflare-pages | none | Cloudflare Pages | |
| deno | token | DENO_TOKEN | JSR (Deno) |
| docker | credential | DOCKER_USERNAME, DOCKER_PASSWORD | Container registry |
| go | none | Go module proxy | |
| hex | token | HEX_API_KEY | hex.pm (Elixir) |
| maven | none | Maven Central / Gradle | |
| maven-central | none | Maven Central (Central Portal) | |
| npm | token | NPM_TOKEN | npm registry |
| pypi | token | PYPI_TOKEN | Python Package Index |
#Class hierarchy
All 10 pipeline implementations inherit from BasePipeline, which provides no-op defaults for publish and build steps plus the shared build_custom_assets() implementation. Two intermediate mixins add authentication patterns: TokenPipeline for single-token auth (5 pipelines) and CredentialPipeline for username/password pairs (1 pipeline).
| Class | Auth pattern | Pipelines |
|---|---|---|
BasePipeline | None (direct subclass) | go (proxy notification), maven (flexible auth), maven-central (Central Portal credentials), cloudflare-pages (selfdoc CLI) |
TokenPipeline(BasePipeline) | Single env var token | npm, pypi, deno, hex |
CredentialPipeline(BasePipeline) | Username + password env vars | docker |
TokenPipeline validates that the token env var is set before attempting publish and passes it to the ecosystem-specific publish command. CredentialPipeline validates both username and password env vars.
#Custom assets
Custom assets allow attaching arbitrary build artifacts to GitHub Releases alongside the source code archive. Each asset has a user-defined build command, an expected output filename, and a configurable maximum file size enforced via max_asset_size_mb (no default -- must be explicitly set when assets are enabled). The complete 7-step flow during rlsbl release run:
- Config defines build commands and output filenames in
custom_assets - Creates distribution directory:
.rlsbl/dist/<pipeline-name>/ - Runs each build command with
$RLSBL_DIST_DIRenv var pointing to the dist directory - Verifies each expected output file exists in
$RLSBL_DIST_DIR - Validates file size against
max_asset_size_mb(hard error if exceeded) - Uploads all artifacts to GitHub Release via
gh release upload <tag> --clobber - Cleans up the dist directory
#Custom assets config example
{
"pipelines": {
"release-bins": {
"type": "go",
"local": true,
"assets": true,
"max_asset_size_mb": 50,
"custom_assets": [
{
"name": "mytool-linux-amd64",
"build": "GOOS=linux GOARCH=amd64 go build -o $RLSBL_DIST_DIR/mytool-linux-amd64 ./cmd/mytool"
},
{
"name": "mytool-darwin-arm64",
"build": "GOOS=darwin GOARCH=arm64 go build -o $RLSBL_DIST_DIR/mytool-darwin-arm64 ./cmd/mytool"
}
]
}
}
}#Capability gating
Pipeline steps are gated on 2 target capabilities (publish and build_assets). Each release target declares which pipeline operations it supports, and rlsbl skips steps the target cannot handle rather than failing. This allows you to configure pipelines broadly without worrying about targets that lack publish or build support — the system gracefully omits inapplicable steps while still executing the rest of the release flow.
| Capability | Effect when absent |
|---|---|
publish | The publish step is skipped entirely for that target |
build_assets | Asset building is skipped for that target |
This means a target that does not support publishing (e.g., a documentation-only target) will not attempt to run any pipeline's publish step, even if pipelines are configured. The pipeline config remains valid — it simply has no effect for that target.
#Migration from old publish key
The old publish key in .rlsbl/config.json is no longer recognized. Running rlsbl release run with a publish key present produces a hard error — no fallback, no deprecation warning. The migration is mechanical: the new pipelines format is a strict superset of the old publish value, adding only a user-chosen name for each entry and an explicit local field. Most projects need fewer than 5 lines changed in their config.
To migrate:
- Read the old
publishvalue (it was a dict withtypeand optionallylocal) - Create a
pipelinesentry with a descriptive name - Copy
typeandlocalfields - Add
token_varif you were using a non-default env var - Remove the old
publishkey
Before:
{
"publish": {
"type": "npm",
"local": false
}
}After:
{
"pipelines": {
"npm-publish": {
"type": "npm",
"local": false
}
}
}#Example configs
#npm publish via CI (most common)
{
"pipelines": {
"npm": {
"type": "npm",
"local": false
}
}
}CI workflow uses NPM_TOKEN secret. No local publish step runs.
#Local Cloudflare Pages deploy
{
"pipelines": {
"docs": {
"type": "cloudflare-pages",
"local": true
}
}
}Publishes from the developer machine using selfdoc's deploy integration. Reads CF_PAGES_API_TOKEN and CF_ACCOUNT_ID from the environment.
#Multiple pipelines
{
"pipelines": {
"registry": {
"type": "pypi",
"local": false
},
"site": {
"type": "cloudflare-pages",
"local": true
}
}
}PyPI publishing happens in CI; docs deploy happens locally in a post-release hook.
#Per-type reference
#npm
- Class:
TokenPipeline - Default token env var:
NPM_TOKEN - Auth pattern: Single token. CI workflow sets
//registry.npmjs.org/:_authTokenfrom the secret. - Publish command:
npm publish --provenance --access public(always uses npm CLI directly for local publish, regardless of which package manager the project uses). - CI template: Detects which package manager the project uses (npm, pnpm, or yarn) and generates the appropriate install and publish steps for that package manager.
- Quirks: Package manager detection is based on lockfile presence (
package-lock.jsonfor npm,pnpm-lock.yamlfor pnpm,yarn.lockfor yarn). Priority order is pnpm > yarn > npm. Detection walks up directories until it finds a.gitdirectory. The detection only affects CI template selection, not the local publish command.
#pypi
- Class:
TokenPipeline - Default token env var:
PYPI_TOKEN(fallback:TWINE_PASSWORD) - Auth pattern: Dual-token fallback. Checks
PYPI_TOKENfirst, thenTWINE_PASSWORD. However, the preferred approach is OIDC Trusted Publishing, which requires no token at all — CI authenticates via GitHub's OIDC provider andpypa/gh-action-pypi-publish. - Publish command:
uv buildfollowed byuv publish(passes token viaUV_PUBLISH_TOKENenv var). No twine fallback. - CI template: Uses
pypa/gh-action-pypi-publishwithid-token: writepermission for OIDC. - Quirks: For new packages, a pending publisher must be configured on pypi.org before the first release. No local
uv publishor token needed when using Trusted Publishing. Overrides the baseTokenPipeline.publish()method to implement dual-token resolution.
#go
- Class:
BasePipeline(no token required) - Default token env var: None
- Auth pattern: No authentication. Go modules are published by pushing a tagged commit — the Go module proxy picks it up automatically.
- Publish command: Notifies the Go module proxy (
proxy.golang.org) by requesting the module at the new version, then runsgo install <path>for every path declared ininstall_paths. - CI template: Minimal — Go publish is just the tag push plus a proxy notification step.
- **Required
artifactkey:** Everytype: "go"pipeline must declareartifact, either"binary"or"library". There is no default. The value selects the publish workflow that gets scaffolded:
- "binary" — a CLI/command whose GitHub Release assets are built by goreleaser (publish.yml). - "library" — an importable module verified against the Go module proxy (publish-library.yml); no goreleaser, no release assets.
A wrong or missing value produces a broken workflow, so validation is a hard error rather than a silent guess. rlsbl scaffold sets the key automatically by auto-detecting the project layout (a project with no package main is a library, otherwise a binary), and the validation error message includes the same auto-detected suggestion — but the operator must commit the choice explicitly.
- Library tag handling: The library publish workflow bakes the module path from
go.modat scaffold time (correct even for monorepo subdirectory modules, whose proxy-visible tags are the companion subdir tag<subdir>/vX.Y.Z) and derives the version from the release tag, handling plain (v1.2.3), releasable (<name>@v1.2.3), and subdir (<subdir>/v1.2.3) tag formats. - Private modules: A private Go module cannot be verified against the public proxy (
proxy.golang.orgrefuses to serve private modules). Private Go libraries must setpublish_mode"none"in.rlsbl/config.json, which suppresses the publish job entirely — no publish workflow is scaffolded. - Quirks: Pipelines with
local: truemust declareinstall_paths(a list of main-package dirs relative to the project root, e.g.["./cmd/mytool"]). Missing or invalid declarations are hard errors; each declared path is validated againstgo list(it must be apackage maindir). There is no auto-detection fallback — detection only validates declarations.
#deno
- Class:
TokenPipeline - Default token env var:
DENO_TOKEN(fallback:JSR_TOKEN) - Auth pattern: Dual-token fallback, similar to pypi. Checks
DENO_TOKENfirst, thenJSR_TOKEN. - Publish command:
deno publish - CI template: Passes the token via environment variable to the publish step.
- Quirks: Publishes to JSR (JavaScript Registry). The dual-token fallback accommodates projects that use either env var name.
#hex
- Class:
TokenPipeline - Default token env var:
HEX_API_KEY - Auth pattern: Single token passed via
HEX_API_KEYenv var. - Publish command:
mix hex.publish --yes - CI template: Standard publish step with the token from GitHub secrets.
- Quirks: Standard single-token pattern. The
--yesflag is required to skip the interactive confirmation prompt.
#maven
- Class:
BasePipeline(flexible auth) - Default token env var:
GITHUB_TOKEN(configurable viatoken_varin pipeline config). - Auth pattern: Single token read from the configured
token_varenv var (defaults toGITHUB_TOKEN). SubclassesBasePipelinedirectly rather thanTokenPipelinebecause it implements its own token resolution with a different default. - Publish command: Detects gradle vs maven build system. Runs
./gradlew publishfor Gradle projects ormvn deployfor Maven projects. - CI template: Generates appropriate publish steps based on detected build system and target registry.
- Quirks: Build system detection is based on the presence of a
gradlewscript (Gradle) orpom.xml(Maven) in the project directory. Errors if neither is found. Does not check forbuild.gradleorbuild.gradle.ktsdirectly.
#maven-central
- Class:
BasePipeline(own credential resolution) - Default credential env vars:
ORG_GRADLE_PROJECT_mavenCentralUsername,ORG_GRADLE_PROJECT_mavenCentralPassword,ORG_GRADLE_PROJECT_signingInMemoryKey,ORG_GRADLE_PROJECT_signingInMemoryKeyPassword - Auth pattern: Four env vars for Central Portal user tokens and GPG signing. All four must be set for local publish. Optional:
ORG_GRADLE_PROJECT_signingInMemoryKeyId(for specific GPG subkey selection). - Publish command: Detects gradle vs maven build system. Runs
./gradlew publishAndReleaseToMavenCentralfor Gradle projects (delegates to the vanniktech/gradle-maven-publish-plugin) ormvn deployfor Maven projects with Central Portal configuration. - CI template: Uses
publish-central.yml.tpl(separate from the maven pipeline'spublish.yml.tpl). Passes credentials as GitHub secrets. - Quirks: Subclasses
BasePipelinedirectly (notTokenPipelineorCredentialPipeline) because it requires four env vars rather than the standard one or two. Build system detection is identical to themavenpipeline (presence ofgradleworpom.xml). Amaven-central-metadataquality check validates POM metadata (name, description, url, licenses, developers, scm), sources/javadoc jar generation, and signing configuration when amaven-centralpipeline is configured.
#docker
- Class:
CredentialPipeline - Default credential env vars:
DOCKER_USERNAME+DOCKER_PASSWORD - Auth pattern: Username and password pair. Both must be set. Configured via
username_varandpassword_varin the pipeline config. - Publish command:
docker buildwith--build-arg VERSION=<version>, thendocker pushwith the versioned tag, thendocker tagto create alatesttag, then pusheslatest. No explicitdocker loginstep in local publish (credentials are validated but login is assumed to be pre-configured). - CI template: Login step followed by build and push steps.
- Quirks: Requires
imageandregistryfields in the pipeline config to construct the full image reference (<registry>/<image>:<version>). Both the versioned andlatesttags are pushed.
#cloudflare-pages
- Class:
BasePipeline - Default token env var: None (uses
CF_PAGES_API_TOKENandCF_ACCOUNT_IDenv vars for local deploys). - Auth pattern: Requires
CF_ACCOUNT_IDandCF_PAGES_API_TOKENfrom the environment when publishing locally. These are reported byrequired_env_vars(). - Publish command:
selfdoc deploy --approve-consequential(requiresselfdocon PATH). No Wrangler fallback.selfdoc deploydeclares itselfconsequential— the deployment is live the moment it lands — so the pipeline passes the skip flag; the approval was already taken byrlsbl release runone level up. - CI template: Minimal — most Cloudflare Pages projects deploy locally from post-release hooks rather than CI.
- Quirks: The simplest pipeline implementation. Primarily used for documentation sites that deploy alongside library releases. Requires
selfdoctool on PATH; errors if not found. 300-second timeout on the deploy command.
#Launcher artifact kind
The artifact: "launcher" pipeline kind produces a wrapper package that downloads a pre-built binary from a GitHub Release. This is for projects that have a Go (or other compiled) binary and want to distribute it via npm and/or PyPI as a convenience shim.
#Config shape
{
"pipelines": {
"go": {"type": "go", "local": false, "target": "go", "artifact": "binary"},
"npm": {"type": "npm", "local": false, "target": "npm", "artifact": "launcher",
"wraps": "go", "binary_source": "github-release", "download": "postinstall",
"provenance": true},
"pypi": {"type": "pypi", "local": false, "target": "pypi", "artifact": "launcher",
"wraps": "go", "binary_source": "github-release", "download": "first-run"}
}
}#Required keys
| Key | Type | Description |
|---|---|---|
artifact | "launcher" | Selects the launcher publish template instead of the standard publish template |
wraps | string | Name of the pipeline that produces the binary. Must reference a pipeline with artifact: "binary". |
binary_source | "github-release" | Where the launcher downloads binaries from. Only "github-release" is supported. |
download | "first-run" | "postinstall" | When the binary is fetched. "postinstall" (npm only) downloads it at npm install time; "first-run" downloads it lazily on the first CLI invocation (zero network I/O at install). No default. |
All four keys are mandatory when artifact is "launcher". Missing or invalid values are hard errors at config validation and scaffold time. download: "postinstall" is an npm-only mechanism -- a non-npm launcher (e.g. PyPI, which has no install-time hook) with download: "postinstall" is a hard error and must use "first-run".
#download mode semantics
The download key selects when the wrapped binary is fetched from GitHub Releases. This is a deployment-shape decision with no default -- the operator must explicitly choose between fetching at install time or lazily on first invocation. Each mode has different trade-offs for network behavior, install speed, and offline usability that affect how end users experience the tool:
- **
postinstall(npm only):** The wrapper ships apostinstallscript (scripts/postinstall.cjs) that runs atnpm installtime. It mapsprocess.platform/process.archto goreleaser's OS/arch naming, downloads the matching release asset and the release'schecksums.txt, SHA-256-verifies the asset against the matchingchecksums.txtline before installing it into the package'svendor/directory, and hard-fails on a checksum mismatch or a 404. Abin/launcher.cjsstub then execs the vendored binary, passing argv through. Node stdlib only -- zero runtime dependencies. - **
first-run(npm and PyPI):**npm installperforms zero network I/O -- nopostinstallscript is emitted. The wrapper ships a single self-containedbin/launcher.cjs(npm) or console-script module (PyPI) that, on the first CLI invocation, resolves the exact package version, downloads the matching release asset andchecksums.txt, SHA-256-verifies before caching, extracts the binary to a platform-specific cache directory (~/.cache/<tool>/on Linux,~/Library/Caches/<tool>/on macOS,%LOCALAPPDATA%\<tool>\on Windows), then execs it -- passing argv through. Subsequent invocations exec the cached binary directly (no network). This is the required mode for consumers whose package must not touch the network at install time (e.g. library-only installs). Stdlib only -- zero runtime dependencies.
PyPI has no postinstall hook, so PyPI launchers always use first-run.
Embedded platform wheels (building the binary into the wheel for each platform) are a different distribution model -- that is the per-platform binary-wrapper family, not the launcher. Launchers are download-at-install/run shims.
#Manifest is the name authority
Scaffold never invents or writes the package name field in the launcher target's manifest (package.json for npm, pyproject.toml for PyPI). The manifest at the launcher target's declared path is the name authority. If the manifest is absent, scaffold hard-errors and directs the user to create it with a rlsbl check-name'd name.
Around that pre-existing manifest, scaffold generates the shim code and fills only the missing non-name fields, exactly once -- never touching the name or any value the user already set, so a second scaffold is a byte-level no-op:
- **npm (
download: "postinstall"):**bin(maps the command name tobin/launcher.cjs),scripts.postinstall(node scripts/postinstall.cjs), andfiles(["bin", "scripts", "vendor"], so the shims ship in the tarball). - **npm (
download: "first-run"):**binandfiles(["bin"]) only. Noscripts.postinstall-- installing the package performs zero network I/O. - PyPI: the
[project.scripts]console-script entry (mapping the command name to the launcher module'smain).
The wrapper-producer check additionally hard-errors if one of these required fields is later deleted from the manifest, naming the field -- a deletion would silently break the published wrapper. The required-field set is download-mode-aware: in first-run mode scripts.postinstall is not required (and not expected), only bin and files.
#Hard constraint: goreleaser default asset naming
Launchers depend on goreleaser's default asset naming and the literal checksums.txt filename. The producer's .goreleaser.yml must emit assets named <ProjectName>_<Version>_<Os>_<Arch>.<ext> (tar.gz, or zip on Windows) and a checksum file named exactly checksums.txt. The scaffolded config does this out of the box.
Both the CI verify step (which probes a representative asset URL and the checksums.txt URL for HTTP 404) and the install/first-run shims (which reconstruct these names to download and SHA-256-verify) are built on this contract. A custom name_template in .goreleaser.yml breaks it and is unsupported: the verify step turns the drift into a red publish job at the release that introduced it, rather than letting silent 404s reach every future install.
#Verification closures
Two structural closures work together to prevent broken wrapper packages from reaching registries. The first closure enforces ordering so the binary exists before the wrapper publishes, and the second closure verifies that the expected download URLs actually resolve. Both are enforced automatically in the generated CI workflows and cannot be bypassed:
- **
needsdependency chain.** Every launcher publish job emitsneeds: [gate, <producer-job-key>]in the generated CI workflow. This ensures the binary producer's publish job (e.g., goreleaser) has finished and uploaded its assets before the launcher attempts to publish. The merged publish generator and the monorepo router both preserve this dependency. Without this, a shim could publish before its binary exists -- a permanently broken package on a registry that cannot un-publish.
- URL verify-before-publish. Before running
npm publishoruv publish, the launcher workflow curls the constructed release-asset URL for a representative platform (linux/amd64) and hard-fails on HTTP 404. This catches goreleaser asset-naming drift (e.g., a customname_templatein.goreleaser.yml) at the release that introduced it, turning it into a red CI job instead of silent 404s for all future installs.
#wrapper-producer check
The wrapper-producer check (registered in the check system under the project and preflight tags) validates that every launcher pipeline's wraps field references an existing pipeline whose artifact is "binary". This runs during rlsbl check and as part of the release preflight, catching misconfigurations before they reach CI.
#Decision rule: launcher vs monorepo members
- One-off wrapper (single Go binary distributed via npm or PyPI): use a subdirectory launcher target. The wrapper's
package.jsonorpyproject.tomllives in a subdirectory (e.g.,packaging/npm/), declared as an explicit target with a path. - Complex multi-artifact (multiple packages that need coordinated versioning): use monorepo members in a shared releasable. Each member gets its own version bump, changelog, and independent publish pipeline. Multi-artifact releasables publish every member at the shared version.
Same-registry multiplicity is not a goal for launchers -- one launcher per registry per project.