One app package serves many versions of the software it installs. A person picking your app from the catalog picks a
version too — 1.7.1, 20.11.0, 3.46 — and your install command is handed that string and asked to produce exactly
it. This page is the author's side of that: how to install a pinned upstream version so that it works, and how to tell
ORC8R which versions exist in the first place. It follows on from Authoring apps.
The version is the upstream project's version
The chosen version reaches your commands as the token ${version}, which any phase may name, as in
["./install-jq.sh", "--version=${version}"]. It carries the upstream version string — the one the project
itself publishes and tags, the same string the catalog offered. It is not a version of your package. Publishing
myapp:default once is enough to serve every upstream release: one package, one settings schema, one set of scripts,
and a version that varies per node.
Two consequences follow, and they are the whole discipline of this page.
Your install script must fetch that exact upstream artifact — the vendor archive, the release asset, the language-registry package for that version. Nothing else honours the choice the operator made.
An OS package manager is not a version source. apt-get install nodejs gives you whatever the distribution
decided, which is not the version asked for and drifts under you. Package managers stay legitimate inside an install
script for system dependencies, but never for the thing the app is named after.
${version} is unset in one case: when the version is the implicit default tag, which is what an app that declares
no versions gets. An element naming it is then dropped whole, so --version=${version} is simply absent, and your
script decides what no version means.
Installing a pinned version
Verify a published checksum
Every artifact your install script fetches must be checked against a checksum the vendor published, and the phase must fail on mismatch. An install script runs privileged on every node of a pool at once, so an unverified download is an unattended remote-code path into all of them. Fetch the vendor's checksum file for that release, look your own filename up in it, and let the checksum tool decide:
curl -fsSLO "$base/$asset"
curl -fsSL "$base/sha256sum.txt" > sums.txt
grep " $asset\$" sums.txt | sha256sum -c - # non-zero exit fails the phase
Keep the layout version-scoped
Several versions of one app may be installed on a node at once; ORC8R keys install state by app and version and imposes no exclusivity. Avoiding collisions is yours to do. Install into a path that carries the version:
| Platform | Layout |
|---|---|
| Unix | /opt/<app>/<version>/ |
| Windows | %ProgramFiles%\<app>\<version>\ |
Two versions can then coexist, an uninstall removes exactly one version's tree, and a half-finished install of a new
version cannot corrupt the running old one. Claim only your own subtree: /opt/<app> may already exist from a
manual install, so create it if missing and treat only <version>/ beneath it as yours.
Put the binary on PATH yourself
The runtime never edits PATH for you; the install phase exposes entry binaries. Unix: symlinks in
/usr/local/bin, each pointing into the versioned tree. That directory is on the default PATH for every kind of
process, and switching the active version is repointing the link: ln -sfn "/opt/jq/$version/bin/jq" /usr/local/bin/jq.
Do not use profile.d fragments or other shell-profile edits: only interactive login shells read them, so systemd
units, phases and CI runners never see the binary.
Windows: a current junction plus one machine PATH entry. Keep %ProgramFiles%\<app>\current pointing at the
active version, and add that one path to the machine PATH at first install, removing it at last uninstall. Switching
versions repoints the junction (New-Item -ItemType Junction -Path "$root\current" -Target "$root\$Version" -Force)
and leaves PATH alone, which matters because a PATH change reaches only processes started after it.
Remove only links you still own
An uninstall removes an exposure symlink or junction only while it points into that version's own tree. Replacing
1.7.1 with 1.8.0 installs the new version first — which repoints the link at 1.8.0 — and then uninstalls 1.7.1. An
uninstall that removed the link unconditionally would delete the working 1.8.0 exposure, leaving the software
installed and nothing on PATH. The uninstall script in the
example below checks ownership first.
Installing X over Y must yield a working X. Coexistence is a should; replacement is a must. Test it: install
one version, install another over it without uninstalling, and confirm the binary on PATH reports the second.
The vendor-layout escape hatch. Some software owns its layout: a self-managing toolchain, an MSI with a fixed target. Use the vendor's layout then, at a stated cost — this app supports no side-by-side installs — and the replacement rule still applies. Do not fight an installer into a versioned path it does not support.
Declaring where versions come from
config.versions tells ORC8R how to enumerate the versions your app can install, so the catalog offers them without
you republishing the package for each release. config.default_version names the one selected by default.
config:
default_version: "3.46"
versions:
source: github_releases
url: https://github.com/jenkinsci/swarm-plugin
filter:
prerelease: false
Sources
source | url | What it enumerates |
|---|---|---|
| omitted | — | The tags of your app's own repository. |
github_releases | GitHub repo URL | Published releases; knows prereleases, skips drafts. |
github_tags | GitHub repo URL | Repository tags. |
gitlab_releases | GitLab project URL | Published releases. |
gitlab_tags | GitLab project URL | Repository tags. |
json | any URL | A JSON document; see below. |
http | any URL | Plain text, one version per line; blank lines and # comments ignored. |
nuget / winget | optional feed URL | Package versions; the package id is the app name. |
Sources are fetched over https with a timeout and a size cap, and the result is capped at a hundred versions: write
a filter that narrows to what you want to offer. A source not in the table is not an error — it enumerates
nothing, which looks exactly like an upstream with no releases. If a list comes back empty, check its spelling first.
A fixed set needs no source, the right answer for versions you control by hand: versions: {list: ["3.46", "3.45"]}.
The json source
json reads a version index that is neither a release list nor plain text, using two fields of its own:
select— a JSON pointer to the node holding the versions (/versions). Omit it for the document root.field— the property carrying the version string, required when that node is an array of objects.
An array of strings is the version list, an array of objects yields each element's field, and an object yields its
keys. Node's distribution index is an array of objects, each with a version like v20.11.0:
versions:
source: json
url: https://nodejs.org/dist/index.json
field: version
filter: {pattern: '^v(?P<version>\d+\.\d+\.\d+)$', latest_per: minor}
The filter pipeline
Every filter field is optional, but the stages always run in this order: the source produces its strings, then
pattern extracts, then prerelease and exclude drop, then sort orders newest first, then latest_per prunes,
then limit truncates.
| Field | Default | Effect |
|---|---|---|
pattern | — | A regular expression the version must match; may also extract. |
prerelease | false | Prereleases are dropped unless true. |
exclude | none | Exact versions to drop, such as a known-broken release. |
sort | semver | semver, numeric or string; always newest first. |
latest_per | none | major, minor or none. |
limit | none | Keep at most N versions after sorting. |
The regular-expression dialect has no backtracking, so evaluation is bounded. An invalid sort, latest_per or
pattern is a loud error. exclude runs after extraction, so it matches the version as extracted.
Extracting a version out of a tag
jq tags releases jq-1.7.1; plenty of projects prefix v. Extraction is opt-in through a named capture group:
filter:
pattern: '^jq-(?P<version>\d+\.\d+(\.\d+)?)$'
That group's text becomes the version: tag jq-1.7.1 becomes 1.7.1, for exclude, sorting, the catalog and
${version} alike. Your install script receives 1.7.1 and rebuilds the tag it needs as jq-1.7.1.
- A pattern with no named group only filters; ordinary groups like
(\.\d+)?extract nothing. - A match in which the named group does not participate is a non-match, and the entry is dropped.
- Every version must be usable as a tag, or it is dropped. A tag like
release/2.4cannot be a version; a pattern that lifts2.4out of it can.
latest_per: one row per release line
| Value | From 0.12.0, 0.11.9, 0.11.8, 0.9.25, 0.9.24 |
|---|---|
none (default) | all five |
minor | 0.12.0, 0.11.9, 0.9.25 |
major | 0.12.0 |
Versions the semver parser cannot read — R2025a, 2024b — pass through unpruned, so a calendar-versioned
project is not collapsed to one entry.
default_version, when left out, is the newest discovered version, usually right for a package that tracks
upstream. Set it when the newest release is not where you want people landing, such as the release you have tested.
A discovered version has no platforms of its own, so list only releases upstream ships for every platform your
package supports.
GitLab prereleases. GitLab marks only future-dated releases as upcoming, so an already-published rc or
beta passes prerelease: false. github_tags and gitlab_tags have no prerelease concept at all. Guard those
sources with a pattern that writes the shape you accept, such as '^v?(?P<version>\d+\.\d+\.\d+)$'.
When not to declare discovery
Leave versions out when the software updates itself (a catalog of its tags is a lie the operator will act on), when
what it installs has no upstream version (the built-in apt app), or when the versions are yours rather than
upstream's. Such an app falls back to the tags of its own repository: publish myapp:2.1 and version 2.1 is offered;
publish only myapp:default and there is one version, default, for which ${version} is unset.
Publish test builds where only you can see them
Every tag you push is a version the catalog offers. Push myapp:trybuild beside myapp:default and everyone who
sees the app is offered trybuild. The scope you publish to is the control: an app is looked up in your project, then
your org, then the apps every org is offered, and the first that publishes it answers. A test build pushed into your
project shadows the shared package for you alone:
$ orc build -t acme/checkout/myapp:default --push # only the acme/checkout project sees it
$ orc build -t acme/myapp:default --push # the whole acme org sees it
End to end: packaging a CLI from GitHub releases
jq tags releases jq-1.7.1, publishes per-platform binaries (jq-linux-amd64), and a sha256sum.txt beside them.
1. The recipe, artifact.yaml. There is no start phase: installing a CLI is work that finishes, and a phase
that exits 0 is reported completed. Each phase runs only the command it declares:
artifactType: application/vnd.orc8r.app.v2
annotations:
org.opencontainers.image.title: jq
org.opencontainers.image.description: Command-line JSON processor
config:
versions:
source: github_tags
url: https://github.com/jqlang/jq
filter: {pattern: '^jq-(?P<version>\d+\.\d+(\.\d+)?)$', latest_per: minor, limit: 20}
install:
command: ["./install-jq.sh", "--version=${version}", "--arch=${arch}"]
uninstall:
command: ["./uninstall-jq.sh", "--version=${version}"]
files: [install-jq.sh, uninstall-jq.sh]
platforms:
- {os: linux, arch: amd64}
- {os: linux, arch: arm64}
2. The install script, install-jq.sh, executable: fetch the exact version, verify, install versioned, expose.
#!/bin/sh
set -eu
version= arch=
for arg; do
case $arg in
--version=*) version=${arg#--version=} ;;
--arch=*) arch=${arg#--arch=} ;;
--help) echo "Usage: install-jq.sh --version=VERSION --arch=ARCH"; exit 0 ;;
*) echo "unknown argument $arg" >&2; exit 2 ;;
esac
done
[ -n "$version" ] && [ -n "$arch" ] || { echo "--version and --arch are required" >&2; exit 2; }
asset="jq-linux-$arch"
base="https://github.com/jqlang/jq/releases/download/jq-$version"
prefix="/opt/jq/$version"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
cd "$work"
curl -fsSLO "$base/$asset"
curl -fsSL "$base/sha256sum.txt" > sums.txt
grep " $asset\$" sums.txt | sha256sum -c -
mkdir -p "$prefix/bin"
install -m 0755 "$asset" "$prefix/bin/jq"
ln -sfn "$prefix/bin/jq" /usr/local/bin/jq
3. The uninstall script, uninstall-jq.sh, parses --version the same way, then removes one version's tree, and
the link only while it is still ours:
prefix="/opt/jq/$version"
case "$(readlink /usr/local/bin/jq 2>/dev/null || true)" in
"$prefix"/*) rm -f /usr/local/bin/jq ;;
esac
rm -rf "$prefix"
rmdir /opt/jq 2>/dev/null || true # only when it is now empty
4. Build, publish and preview. Publish under default: discovery reads its versions block from that tag.
$ orc build -t jq:default --push
Resolved reference: orc8r.com/jq:default
$ orc versions jq
VERSION DEFAULT PLATFORMS
1.7.1 * any
1.6
1.5
orc versions runs the server's pipeline against the live upstream and reports a source failure rather than a short
list. An empty list means a misspelled source, a named group that did not participate, or versions that are not tags.
5. Use it. Attaching the app to a pool at version 1.6 runs install-jq.sh --version=1.6 --arch=amd64 on every
amd64 node, which fetches tag jq-1.6, verifies it, installs it at /opt/jq/1.6/ and links it onto PATH.
Related pages
- Authoring apps — the rest of the package: settings, phases and publishing.
- App commands and values — tokens, and how values reach your scripts.
- Pools — choosing an app version when you request nodes.
- Projects — pinning a version so everyone in a project gets the same one.