Branches and Releases
Work collects on dev, and a release pull request moves it to main,
which builds and deploys. Pushes and merges to any other branch run
nothing.
Where work lands
A merge to main after a version bump publishes the lnk release
(release.yml, on macOS runners, which cost the most). So main only
moves when a release is ready. No workflow publishes the relay: an
operator builds it (Relay).
dev
Where work lands. Branch from dev, with one branch and one PR per
batch of related work, each change its own commit, and the PR against
dev. Don't bump the version in these PRs. Say in the description how
to try the branch.
A PR's Release notes
Fill in the PR template's Release notes (Breaking, Added or Fixed, one line per user-visible change) and its Semver line, and keep them and the description current as commits are added, because the release PR reads them to pick the version.
main
What's released. It takes release PRs from dev, and the rare hotfix
described below.
Trying unreleased work
git fetch && git checkout <branch> && cargo build, or an install of
lnk and its plugins from a branch with cargo, as
Install lnk says.
Cutting a release
A release is a version bump on dev and a pull request from dev into
main.
- List what
devadds: the PRs merged intodevsince the last release (git log --merges --first-parent origin/main..origin/dev), and their Release notes. - Pick the version from the largest change, against the current
versionin the public workspace'sCargo.toml. Before 1.0, Breaking or Added bumps the minor version (0.11.0 -> 0.12.0) and Fixed bumps the patch (0.11.0 -> 0.11.1). From 1.0, Breaking is major, Added minor and Fixed patch. If nothing is user-visible, merge without a bump, and nothing is published. - On
dev, commit the bump (The bump). That commit is the only change the release makes itself. - Open a PR from
devtomaintitledRelease v<version>. Its body is the combined Release notes (Breaking, Added, Fixed), each line with its PR number. - Before merging, run the checks on
dev's head.checks.ymlruns them again after the merge and publishes nothing if they fail. - Merge with a merge commit, never a squash or rebase, or
devandmainstop sharing history. The merge publishesv<version>.
The bump
version under [workspace.package], then cargo build so
Cargo.lock follows. The release also ships Link Harness, Link's
tools, the OpenClaw, Hermes and DeepSeek Harness adapters and the
connections' plugins, which lnk installs only at its own version.
Every public package inherits the workspace version.
set-version.sh
From core/, ../../.github/set-version.sh ../.. <version> changes it
and updates the one lockfile's workspace entries offline. Fetch
dependencies first if they are not cached.
A hotfix
A hotfix that can't wait for dev branches from main and goes in
with its own PR and a patch bump. Then main is merged back into dev.
What a release publishes
A release publishes lnk with its plugins, signs it, and deploys the
website.
checks.yml
Runs fmt, clippy, tests and shellcheck on release PRs into main
alone; website.yml builds the website for them. Nothing into dev
runs CI, dependency updates included: Actions minutes cost money.
release.yml calls checks.yml first and publishes nothing if it
fails.
lnk (release.yml)
Publishes when the workspace version has no release yet. It makes
v<version>, the latest release of so-local/link-releases, with one
archive per program and platform (lnk-core-<target> and
lnk-<plugin>-<target>) for macOS and Linux, each on arm64 and x86_64,
with Linux arm64 built on GitHub's arm64 runner, all under one signed
SHA256SUMS.
Its plugins
The plugins come from local Cargo component declarations, as
scripts/release/components.sh lists them, but a plugin a release
holds back (What a release
ships),
which only a QA or dev build packages. An explicit LNK_COMPONENTS
build input adds first-party component directories to the core's trusted
registry and the product's archives. Build, packaging and sealing read
the same selection, so a declared setup component has a signed asset.
How users get it
Users get lnk from curl -fsSL https://local.link/install.sh | sh,
which the relay serves from core/cli/install.sh, built into its
binary. They get plugins from lnk up and lnk plugin add, and
updates from lnk upgrade. The relay advertises its own version as the
latest client in Welcome, so after a bump connected CLIs print
Update lnk X is available.
The website
The last step of release.yml calls the VERCEL_DEPLOY_HOOK secret,
so the website goes live with the release it describes. Without the
secret it warns and skips.
The bare domain
The bare domain's /_link/* and /install.sh belong to the relay.
With LINK_WEBSITE set, which is --website in bootstrap, any other
path there redirects to the website (link-web, on Vercel).
How a release is signed
Both releases sign SHA256SUMS with the RELEASE_SIGNING_KEY secret,
using the repository's link-public/scripts/release/sign-release.sh.
Sealing
lnk's is sealed by link-public/scripts/release/seal-release.sh, as
a QA build is: it checks the archives are exactly what the build makes,
adds install.sh trusting the signers files it's given (the release
key's alone), VERSION and SHA256SUMS, and signs them, checking the
signature against the last signers file.
Only from main
The job runs only from main, in the release environment, which only
main may use and a reviewer approves. release.rs checks that every
job using a secret does the same.
Built without a cache
The jobs that build what's signed restore no cache, so a poisoned cache
can't alter a release, and release.rs checks that release.yml
names none. A release's build compiles from the checkout
alone, slower than a cached one.
Who trusts the key
Relay servers, lnk upgrade (link_plugin::release) and install.sh
accept only the keys in scripts/release/release-signers. install.sh
and bootstrap.sh embed a copy, and tests/e2e/tests/release.rs
checks they match. A release trusts that key alone.
build-release.sh
release.yml's build runs link-public/scripts/release/build-release.sh
for each target with LNK_BUILD=release, the build and its archives,
which a QA build runs too. Built for the machine it runs on, its
programs must run, say the core's version, and lnk be a QA build
exactly when LNK_BUILD says so.
Package existing programs
package-release.sh packages the selected components from their owning
workspaces' target directories. The builder calls it after compiling;
local QA can package checked native debug programs with
package-release.sh <target> debug .. The same archive helper is used
by isolated signed-release fixtures. Sealing still requires the complete
selected set for every target and the release's expected signer.
A QA build
A QA build is a release in every way but its key, so what fetches a
release (install.sh, lnk upgrade, lnk plugin add) is checked
before the release. It's built from a commit with the release's own
packaging, on your machine or a cloud session, never by a workflow, and
published to a bucket of your own laid out as a release.
Its key
It's signed with the QA key, whose public line is in
scripts/release/qa-signers. lnk --version says on its second line
when it's a QA build, and its install.sh trusts both keys. A
release's binary holds none of the QA key's bytes.
LNK_BUILD
What a build is, LNK_BUILD says as it's compiled, read once
(link_plugin::Build): release (release.yml sets it), qa, dev
(A dev build), or unset for a build from source; any
other value doesn't compile.
Which keys it trusts
Which keys lnk trusts follows from it (release::TRUSTED_SIGNERS): a
QA build trusts the release key and the QA key, so it installs QA
builds and releases; any other, as every release, the release key
alone, so a release never installs a QA build.
What release.rs checks of it
That no workflow builds a QA build or names the QA key, that the QA key
isn't the release key, and that a QA build installs with its own
install.sh and is refused by a release's.
Its version
Its own: the release's it comes before, then -qa. and when it was
built, in UTC to the minute (0.15.0-qa.202610131900). So two QA
builds never share one, a later one is newer than an earlier one, and
the release is newer than both, as lnk upgrade and a move's upgrade
on a box order them. Each build is a folder of its own, named by its
commit.
Make a QA build
qa-release.sh builds, seals and uploads one:
# from core/, on a clean checkout of the commit, with the QA key in a file only you can read
LNK_QA_SIGNING_SECRET_FILE=~/.config/link/qa-signing-secret ../../link-public/scripts/release/qa-release.sh --to s3://my-qa-builds/qa
# or a Google Cloud bucket, or a folder here served over https at the url you give
LNK_QA_SIGNING_SECRET_FILE=~/.config/link/qa-signing-secret ../../link-public/scripts/release/qa-release.sh --to gs://my-qa-builds/qaIts worktree
It builds the commit in a worktree of its own outside the checkout, its
locked crates fetched first (cargo fetch --locked), so a fresh
checkout needs nothing cached, its version set in the public
workspace's Cargo.toml and lockfile (set-version.sh), and removes
the worktree and what it made however the build ends: the checkout is
left as it was.
Its targets
It builds Linux on x86_64 and arm64 (musl) with build-release.sh and
LNK_BUILD=qa, the target that isn't this machine's with cargo zigbuild (pip install ziglang, cargo install cargo-zigbuild), so
this machine's programs are checked as a release's are and another
target's said to be unchecked. --targets picks others. The macOS
targets are built on a Mac, the same way, with --targets.
Sealing and uploading
It seals them with seal-release.sh, as a release is, trusting the
release key and the QA key and signed with the QA key, and uploads them
to <folder>/<commit>/download/v<version>/ and
<folder>/<commit>/latest/download/, the checksums last.
--built
--built <dir> --version <version> seals and uploads archives
build-release.sh made before, once an lnk among them that runs here
says it's a QA build of that version, in a folder named built- and
the start of their checksums' hash.
The key file
The key is a file of yours (chmod 600, never a link, and yours),
copied into place only to sign and removed straight after. The build
runs without its name in its environment, and it's refused in the
environment as LNK_QA_SIGNING_SECRET, where the build, the lnk it
checks and the clouds' CLIs would inherit it.
Keeping the key from the build
The file is still yours to read while it builds, so a build's code that
went looking could find it: to keep it from the build entirely, build
first, fetch the key, then seal with --built.
What a seal trusts
A release's version is sealed trusting the release key alone, and a QA
build's signed by the QA key, whatever signers files seal-release.sh
is given.
Where it's put
<to>/<commit> is where lnk reads it, as it is: a bucket of yours
with your own login, so it needn't be public, or a folder here (Where
releases are read from). --url names
a public https:// address it's served at instead.
Install a QA build
It ends saying how to install it; from a private bucket, a copy here
first, then its own install.sh run against the copy:
aws s3 cp --recursive --only-show-errors s3://my-qa-builds/qa/<commit>/download/v<version> ~/.cache/lnk-qa/<commit>/download/v<version>
LNK_RELEASES_URL=file://$HOME/.cache/lnk-qa/<commit> LNK_VERSION=<version> sh ~/.cache/lnk-qa/<commit>/download/v<version>/install.sh
# from a QA build already, since the lnk of a release refuses one
LNK_RELEASES_URL=s3://my-qa-builds/qa/<commit> lnk upgrade <version>On Google Cloud
The copy is gcloud storage cp --recursive into
~/.cache/lnk-qa/<commit>/download/. The copied install.sh checks
the signature on SHA256SUMS with the keys sealed into it, and each
archive against the signed sums, as from anywhere.
From a public address
curl -fsSL <url>/download/v<version>/install.sh | LNK_RELEASES_URL=<url> LNK_VERSION=<version> sh, checked the same way.
A dev build
A dev build is a QA build whose second key is yours, not the QA key:
for testing your own commit of dev on boxes without Link's keys or a
push to staging.
Why boxes need one
lnk box create installs lnk on the box from the releases this
machine's lnk reads, so a build from source, which has no published
release, can't make a box; a dev build is one, published to a bucket of
your own. The three kinds: main builds releases, staging QA builds,
and each contributor dev builds from dev.
Its key
An Ed25519 key of yours (ssh-keygen -t ed25519), never committed,
never the release key or the QA key. lnk --version says on its second
line when it's a dev build.
LNK_DEV_SIGNER
LNK_BUILD=dev builds lnk trusting the release key and the one
allowed-signers line in LNK_DEV_SIGNER, read as it's compiled
(release::TRUSTED_SIGNERS): a dev build without it, or with anything
but one link-release namespaces="link-release" ssh-ed25519 <key>
line, doesn't compile, rather than trust nothing or anything. No other
build reads it, so a release or a QA build never installs a dev build.
What release.rs checks of it
That no workflow builds one or names LNK_DEV_SIGNER, that no
committed file holds a release signer that isn't the release key, the
QA key or a test's, and that a dev build installs with its own
install.sh and is refused by a release's and by the lnk of a
release and of a QA build.
Its version
A QA build's, with -dev.: the release's it comes before, then when it
was built (0.15.0-dev.202610131900), ordered as a QA build's is.
Make a dev build
For a single-command local build and cloud box, use Developer Boxes. The steps below expose the release location and signing options for custom workflows.
Custom release workflows
dev-release.sh is qa-release.sh with your key: it derives the
signer line from the key, builds with LNK_BUILD=dev and that line in
LNK_DEV_SIGNER, seals trusting the release key and yours, and signs
with yours.
ssh-keygen -q -t ed25519 -N '' -C lnk-dev -f ~/.config/link/dev-signing-secret
# from link-public/, on a clean checkout of the commit
scripts/release/dev-release.sh --key ~/.config/link/dev-signing-secret --to s3://my-dev-builds/dev
# then, with that build of lnk installed here, boxes install it
LNK_RELEASES_URL=s3://my-dev-builds/dev/<commit> lnk box create --cloud awsAs a QA build's
The key file is handled as the QA key's is (chmod 600, never a link,
copied into place only to sign, removed after), and its other options,
the bucket's layout, <folder>/<commit>/..., and how to install it
here are qa-release.sh's. The bucket is yours and private: nothing
about it is public, and a box needs no login to it.
On a box
The box's install.sh is checked against your lnk's keys before the
box runs it, and trusts the keys sealed into it, so it installs your
build. Your lnk reads the bucket with your aws or gcloud login
and copies the release to the box over its SSH, where it installs from
the copy; every lnk command run on the box follows the same
LNK_RELEASES_URL.
Where releases are read from
lnk installs from Link's own releases unless LNK_RELEASES_URL names
others (release::Source). A release, a QA build and a dev build all
take all four: what's installed is trusted only as the signature on its
SHA256SUMS says, checked against the keys lnk was built with, never
by where it's stored.
https://<address>
Read over HTTPS, at an address anyone may read: it holds no
login, \, query or fragment.
s3://<bucket>/<folder> and gs://<bucket>/<folder>
Read with aws s3 cp or gcloud storage cat, as your aws or
gcloud is logged in.
file:///<folder>
The folder here. Each part of a folder is letters, digits or ._~+-,
never . or ..; anything else is refused.
A cloud's login
A cloud's CLI reads its own settings and files, and is given the
variables of its login, which Link hands no other program:
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN,
or CLOUDSDK_AUTH_ACCESS_TOKEN and GOOGLE_OAUTH_ACCESS_TOKEN.
On a box
A box can't read a bucket with your login, or a folder here, so lnk box create copies the release it installs to the box first: its signed
SHA256SUMS and signature, VERSION, install.sh, and every archive
for the box's processor, each checked against the signed sums, to
~/.local/share/lnk/releases/download/v<version> there.
Reading the copy
The box installs from that copy, checking each signature again, and
every lnk that lnk box run runs there reads it as
LNK_RELEASES_URL=file://$HOME/.local/share/lnk/releases. It holds the
version the box was made with; a box for another version is made
again.