Skip to main content
Version: Next

Script environment variables

This page lists every DISPAT_* variable a stage script receives and what it holds. A script sees three layers of variables in this order:

  1. the parent environment, which the .env file fills in where nothing else set a name;
  2. any static env variables the configuration sets;
  3. the variables in the table below, computed from the release plan.

The order matters when a name appears twice. dispat places the static variables first, so a computed DISPAT_* variable always wins. A static value referring to one, such as custom_$DISPAT_VERSION, expands against the values in this table before the script starts.

The same variables expand in record text. A changelog footer and a publish script name a release the same way.

These variables come from the release plan, so a release ordinarily produces them. Run dispat exec --env both to compute the same plan on demand. This lets you run a script written against $DISPAT_VERSION on its own without releasing anything.

VariableExampleMeaning
DISPAT_PACKAGEcorePackage name.
DISPAT_SPACElibsSpace name.
DISPAT_OLD_VERSION1.2.3The version the package last published. It holds 0.0.0 for a first release.
DISPAT_NEW_VERSION1.3.0-beta.4Version being released. It includes the version, channel, and counter in SemVer spelling.
DISPAT_VERSION1.3.0The core version alone. It holds MAJOR.MINOR.PATCH with the channel and counter stripped.
DISPAT_MAJOR1The first number of DISPAT_VERSION on its own.
DISPAT_MINOR3The second number of DISPAT_VERSION on its own.
DISPAT_PATCH0The third number of DISPAT_VERSION on its own.
DISPAT_TAG_VERSION1.3.0-beta4Version, channel, and counter as the space's tagFormat spells them. Read the note below.
DISPAT_STABLE_BASELINE1.2.3The last release with no prerelease component. dispat computes versions from this baseline.
DISPAT_BASELINE1.3.0-beta.3The latest baseline. It holds the newest tag of any kind, including prereleases. It is Unset when the package has never released. Read the note below.
DISPAT_BUMPminorHolds none, patch, minor, or major. It holds none on a channel-only release.
DISPAT_CHANNELbetaThe channel being released on. It holds stable or a prerelease identifier.
DISPAT_OLD_CHANNELstableThe channel of the previous release. This distinguishes a graduation from an ordinary release.
DISPAT_COUNTER4The prerelease counter of the version being released. It is Unset on a stable release.
DISPAT_OLD_COUNTER3The prerelease counter of the previous release. It is Unset when the previous release was stable.
DISPAT_IS_PRERELEASEtrueHolds true when DISPAT_NEW_VERSION carries a prerelease component. Use this to choose a dist-tag.
DISPAT_TAGcore@v1.3.0-beta4The tag that will be created on success. It includes the name, version, channel, and counter rendered with the space's tagFormat.
DISPAT_SEMVER_TAGcore@1.3.0-beta.4The same name, version, channel, and counter under the normative {name}@{version} SemVer format. This ignores what tagFormat encodes. A script can rely on this spelling across spaces.
DISPAT_STAGEbuildWhat is currently running. The spellings are listed below the table.
DISPAT_OUTPUT(a temp file path)Where the script appends NAME=value or DISPAT_OUTPUT_NAME=value lines. This lets you export outputs for everything that runs after it.
DISPAT_OUTPUT_<NAME>(exported value)One variable per accumulated script output. DISPAT_OUTPUTS lists the exported names and is set even when empty.
DISPAT_OUTPUT_SOURCE_<NAME>core:buildThe script that exported or last re-exported <NAME>. It holds <package>:<stage>, or <space>:login for a login export.
DISPAT_EXPORT_GITHUB/pkg/dist/app.tgzSet once a script exported it. This is the opt-in for the package's GitHub release, and its value is the asset list. It travels under its full name and stays out of DISPAT_OUTPUTS.

DISPAT_STAGE carries version, build, publish, or announce for a stage script. It holds the hook's name (beforeBuild, postPublish, postAll, ...) for a hook. It holds login for the login, syncLock for an autoVersion.syncLock script, and run:<name> for dispat run <name>.

DISPAT_TAG_VERSION is the version section of DISPAT_TAG without the name and its decoration. It has no v prefix and no path. It equals DISPAT_NEW_VERSION under formats that leave the prerelease inside {version}.

DISPAT_MAJOR, DISPAT_MINOR, and DISPAT_PATCH split DISPAT_VERSION so a script never has to cut a version string apart. You write a moving series tag from them. This matches how container images write image:1 and image:1.4 beside image:1.4.2. All three are always set, even when they are 0. They describe the core version. A 1.3.0-beta.4 release reports 1, 3, and 0, which is the stable release the train is heading for.

DISPAT_BASELINE is what the computed version must exceed. dispat reads the channel from it. It is unset, not empty, for a package that has never released. Use ${DISPAT_BASELINE+x} to detect a first release. When set, it equals DISPAT_OLD_VERSION.

DISPAT_OLD_VERSION and DISPAT_STABLE_BASELINE differ only on a prerelease train. A package on 1.3.0-beta.1 whose last stable release was 1.2.3 reports both. The first is what it shipped, and the second is what dispat computes the next version from.

The counters are left unset, not empty, when there is nothing to report. A shell's ${DISPAT_COUNTER+x} distinguishes a stable release from a prerelease whose counter happens to be empty text. An empty string cannot do this. An exact Release-As may carry more than the bare number. A version like 2.0.0-rc.1.hotfix reports a counter of 1.hotfix, because the counter is everything after the channel.

Workspace data

Every stage additionally receives two per-package listings. You can read them from any shell without a parser. The version stage is where manifests are reconciled. A build baking versions into artefacts and a publish choosing dist-tags read the same state. Identical environments keep a script movable between stages.

Both listings address packages through a <KEY>. This is the package name uppercased with everything outside [A-Z0-9] replaced by _. For example, @acme/ui becomes _ACME_UI. This happens because a package name may contain bytes a variable name cannot. The raw name always travels in the _NAME field. To look up a package by name, write for k in $DISPAT_WORKSPACE_PACKAGES, compare _NAME, and read the fields. Two names might sanitise to the same key, like core-utils and core.utils. The first in plan order keeps the key, and dispat omits the loser from the listings with a warning. Rename one of the pair if you hit this.

The workspace listing covers every workspace package with the version it will carry at the end of the run. This is its planned version where it is releasing, or its baseline otherwise.

DISPAT_WORKSPACE_PACKAGES="CORE UTILS" # keys in plan order: for k in $DISPAT_WORKSPACE_PACKAGES
DISPAT_WORKSPACE_CORE_NAME="core" # the raw package name
DISPAT_WORKSPACE_CORE_VERSION="1.3.0"
DISPAT_WORKSPACE_CORE_CHANNEL="stable"
DISPAT_WORKSPACE_CORE_RELEASING="true"

The breadth matters. dispat has no manifest model, so reconciling declared dependency ranges is the version script's job. A correct reconciliation cannot be restricted to packages released in the same run. A dependency may have been published by an earlier run whose dependent leg failed. This is exactly the catch-up case. _RELEASING=false with a version newer than the range you declared is that situation. Reconciling against every workspace dependency closes this gap. It is a no-op whenever the narrow rule would already have been right.

The updated-provider listing covers every provider whose version this package picks up in this run:

DISPAT_UPDATED_PACKAGES="CORE" # empty (not unset) when nothing was updated
DISPAT_UPDATED_CORE_NAME="core"
DISPAT_UPDATED_CORE_SPACE="libs"
DISPAT_UPDATED_CORE_OLD_VERSION="1.2.3"
DISPAT_UPDATED_CORE_NEW_VERSION="1.3.0"
DISPAT_UPDATED_CORE_CHANNEL="stable"

"Picks up" is deliberately wider than "was bumped by". A provider that releases alongside this package for its own reasons is listed, even with no propagation between them. The two ship together, so the consumer's manifests still have to name the new version. That is the ordinary case. Propagation reaches nobody unless a commit or the configuration asks it to. A provider released by an earlier run whose consumer leg failed is listed too. Its OLD_VERSION equals NEW_VERSION because the version is already out, and this run is only now picking it up. A provider that is not releasing at all is not listed, since it published nothing to pick up.

dispat filters out providers that failed or were skipped, because their versions were never released. The listing is resolved per stage. A provider can fail between this package's build and its publish. Each stage sees the truth of its own moment. If a package has providers to pick up and none of them survive, the version script specifically does not execute. There is nothing to sync manifests to.

A package released on stable whose dependency currently carries a prerelease version is the one case no range can make honest. Graduate the provider too, or do not graduate the consumer yet.

Release notes data

Every stage and hook of a package also receives its release notes. dispat groups them exactly as the changelog file and the GitHub release group their sections. Units bumping major are breaking changes, minor are features, and patch are fixes:

DISPAT_BREAKING_CHANGES="drop the old API" # one headline per line
DISPAT_FEATURES="add streaming
add retries"
DISPAT_FIXES="close a leak"

Entries are the unit descriptions. They are newline-separated and in history order. A group with no entries is empty text. It is set, not unset, so a line-wise loop iterates zero times. Bodies are omitted, because they are multiline prose that would destroy the line-per-entry contract. They stay in the changelog and the GitHub release. The groups follow the release-notes windowing. On a prerelease, they carry only the release's own changeset. On a stable release, including a graduation, they carry the whole pending window. The dependencies section travels the same way:

DISPAT_DEPENDENCIES="core: 1.2.3 -> 1.3.0" # one "name: old -> new" line per live provider update

This matches the changelog's rendering. From equals To on a catch-up, whose provider version is already out. The DISPAT_UPDATED_* listing carries the same data field by field for scripts that want it addressable. The flow.announce stage is the natural consumer. Like every listing, the variables reach every stage, keeping scripts movable.

Script outputs

Every per-package script and hook receives DISPAT_OUTPUT. This includes the stages, their hooks, the announce frame, onFail/onSkip, and the space's login. It holds the path of a file. You can append NAME=value lines to it, GITHUB_OUTPUT-style, to export values for everything that runs after it. You can write the name bare or already carrying the DISPAT_OUTPUT_ prefix. Both spellings address the same output:

echo "DISPAT_OUTPUT_IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' img)" >> "$DISPAT_OUTPUT"
echo "IMAGE_DIGEST=..." >> "$DISPAT_OUTPUT" # the same output, bare spelling
echo "DISPAT_EXPORT_GITHUB=$PWD/dist/app.tgz $PWD/dist/SHA256SUMS" >> "$DISPAT_OUTPUT"

Outputs accumulate across the package's pipeline into one store. Every later script and hook of the package receives each export as DISPAT_OUTPUT_<NAME>. This includes the outcome scripts onFail and onSkip, so a notifier can report with them.

Two more variables come along. DISPAT_OUTPUTS lists the exported names, space-separated. It is set but empty when nothing was exported. DISPAT_OUTPUT_SOURCE_<NAME> names the script each export came from. It holds <package>:<stage>, like core:build or base:run:lint, or <space>:login for the login.

How far an export travels depends on which script made it. Hooks export exactly like stage scripts. A beforeBuild export reaches the build, the publish, and everything after. The login script's exports are space-scoped. They reach every package of the space from its publish stage onward, because that stage waits for the login. In dispat run, outputs additionally carry across packages from a provider's script to its consumers'. In a release run, they stay within the package. A consumer's release scripts read a provider's new version from the DISPAT_UPDATED_* listing rather than from the provider's outputs.

Re-exporting a name overrides its earlier value and source. This works like a shell re-assignment.

The name must be a valid environment variable name. Other DISPAT_-prefixed names are reserved, so an export cannot shadow the DISPAT_* environment. A malformed line fails a release-gating sequence. It only warns in a warn-only sequence. A sequence that fails still surrenders whatever it exported before failing. This is how onFail gets to see it.

One export is a directive to the GitHub recorder: DISPAT_EXPORT_GITHUB. A package whose scripts exported it gets a GitHub release. The recorder skips a package that never exported it.

Its value is a whitespace-separated list of absolute paths to existing files. dispat uploads each as an asset of the release and names it after the file. $PWD inside a script resolves to the package folder, which makes absolute paths easy. An empty value creates the release with no assets. dispat skips an invalid entry, like a relative path, a missing file, or a directory, with a warning. The release and the sound entries still go through.

Unlike ordinary outputs, this export travels to later scripts under its full name. Appending to it reads echo "DISPAT_EXPORT_GITHUB=$DISPAT_EXPORT_GITHUB $PWD/more.tgz" >> "$DISPAT_OUTPUT". It does not appear in DISPAT_OUTPUTS. It reaches later scripts as a plain environment variable. The dispat github step command run from one of them reads the same opt-in and the same asset list out of its own environment.

The other export with a consumer inside dispat is PACKAGE_<KEY>. The <KEY> is the exporting package's own key under the scheme above. A release script that exports PACKAGE_<KEY>=<commitHash> pins the package's release to that commit. dispat creates the tag there instead of at HEAD, or at the release commit in commit mode. The package's GitHub release carries the hash as its commit and target_commitish. This is meant for packages whose release scripts produce their own commit, like a subtree push or a generated repository, that the tag should point at. Like any output, it reaches later scripts as DISPAT_OUTPUT_PACKAGE_<KEY>:

echo "PACKAGE_CORE=$(git rev-parse HEAD)" >> "$DISPAT_OUTPUT"

Run outcome data

The run-level hooks additionally receive the run's outcome. dispat renders these with the same <KEY> scheme:

DISPAT_PUBLISHED_PACKAGES="CORE" # keys of published packages
DISPAT_FAILED_PACKAGES="UI" # keys of failed packages
DISPAT_SKIPPED_PACKAGES="APP" # keys of packages skipped because a provider failed
DISPAT_CANCELLED_PACKAGES="" # keys of packages an interrupted run never ran
DISPAT_UNPLANNED_PACKAGES="UTILS" # keys of packages the plan did not release
# (unchanged, or held by Release-As: none)
DISPAT_RESULT_CORE_NAME="core" # one block per planned package
DISPAT_RESULT_CORE_STATUS="published" # published / failed / skipped / cancelled
DISPAT_RESULT_CORE_OLD_VERSION="1.2.3"
DISPAT_RESULT_CORE_NEW_VERSION="1.3.0"
DISPAT_RESULT_CORE_CHANNEL="stable"
DISPAT_RESULT_UI_FAILED_STAGE="build" # failed packages only
DISPAT_RESULT_APP_BLOCKED_BY="ui" # skipped packages only: the provider to blame

The five list variables are set even when empty. A shell for-loop iterates zero times instead of reading an unset variable. _FAILED_STAGE and _BLOCKED_BY are unset when there is nothing to report. Unplanned packages carry no DISPAT_RESULT_* block. Their state is the workspace listing's baseline entry. A cancelled status means the run was interrupted, like a Ctrl-C or a killed CI job, before the package ran. Nothing about it failed. The next run picks it up unchanged.

Size

One package costs ~250 bytes across its listing variables. A 500-package monorepo puts roughly 125 KB into each script's environment. Each individual variable is tiny, so the ceiling is total environment size. This is ~2 MiB on Linux and 1 MiB on macOS. That leaves room for a few thousand packages. This is far beyond the size at which one dispat workspace has usually become several.