Tangled release automation #
Reusable release-PR and publishing commands for Tangled Spindles. They provide the release loop normally handled by hosted CI actions while leaving configuration in each repository's workflow.
Warning
This project is experimental. Tangled and Spindles are evolving quickly, and the automation performs privileged release operations including force-pushing a release branch, publishing artifacts, and creating tags. Validate it on a non-critical release before relying on it in production.
Two commands are packaged as a Nix flake:
tangled-release-prmaintains a long-runningchore/releasebranch and its pull-request rounds.tangled-publishpublishes a merged release, optionally deploys it, and pushes its tags.
Spindle installs them directly from this repository with its native flake dependency support:
engine: microvm
image: nixos
dependencies:
- git+https://tangled.org/natemoo.re/automation#release-pr
- git+https://tangled.org/natemoo.re/automation#git-cliff
- git+https://tangled.org/natemoo.re/automation#tg
steps:
- name: Maintain release PR
command: tangled-release-pr
The consumer repository must have a Spindle selected in its Tangled Settings → Pipelines.
Adding spindle to the repo record through the CLI does not currently activate the hosted runner.
Adapter tools are separate flake packages: #git-cliff, #pnpm, #railway, and #tg. Add only
the ones used by that workflow (#tg is needed for automatic PR rounds). This keeps a Docker
publish from realizing Node/pnpm, an npm publish from realizing Railway, and branch-only release
automation from compiling tg.
Supported adapters #
| Concern | Adapter | Behavior |
|---|---|---|
| Changelog | changesets |
Installs the frozen pnpm workspace, then runs pnpm changeset version; pending changeset files determine whether a release is needed. |
| Changelog | git-cliff |
Computes the next 0.x-aware SemVer, regenerates CHANGELOG.md, and writes VERSION. |
| Publish | npm |
Installs the exact release workspace, runs pnpm changeset publish, repairs package tags, and pushes them. |
| Publish | docker |
Builds and pushes immutable vX.Y.Z plus a configurable moving tag to Docker Hub. |
| Publish | command |
Runs repository-owned idempotency and publish commands. Useful for Homebrew or binary releases. |
| Publish | none |
Creates only the release tag. Useful when the tag itself distributes a Nix flake. |
| Deploy | railway |
Redeploys a Railway service from its configured source after a new publish. |
| Deploy | command |
Runs a repository-owned post-publish deploy command. |
The deliberately supported combinations are:
changesets+npmgit-cliff+dockergit-cliff+commandgit-cliff+none
Railway or a custom deploy command can follow any successful publish.
Configuration #
Configuration is inline environment in the consumer workflow. Secrets come from Tangled's repository settings.
Release PR #
| Variable | Required | Default |
|---|---|---|
AUTOMATION_REPOSITORY |
yes | Tangled handle/repo, such as lgtm.shop/commerce |
AUTOMATION_BOT_ACCOUNT |
for automatic PR rounds | Tangled handle used with TG_APP_PASSWORD |
AUTOMATION_BOT_NAME |
yes | Git commit author name |
AUTOMATION_BOT_EMAIL |
yes | Git commit author email |
AUTOMATION_ORIGIN_URL |
no | git@tangled.org:<AUTOMATION_REPOSITORY>; override for a self-hosted knot |
AUTOMATION_CHANGELOG |
no | changesets |
AUTOMATION_DEFAULT_BRANCH |
no | main |
AUTOMATION_RELEASE_BRANCH |
no | chore/release |
AUTOMATION_PR_TITLE |
no | chore: release |
AUTOMATION_VERSION_FILE |
no | VERSION |
AUTOMATION_CHANGELOG_FILE |
no | CHANGELOG.md |
KNOT_DEPLOY_KEY |
yes | SSH private key registered to the bot account |
TG_APP_PASSWORD |
for automatic PR rounds | Bot account app password |
Without TG_APP_PASSWORD, the release branch is still pushed but its PR must be maintained
manually.
Publishing #
| Variable | Required | Default |
|---|---|---|
AUTOMATION_CHANGELOG |
no | changesets |
AUTOMATION_PUBLISH |
no | npm |
AUTOMATION_DEPLOY |
no | none |
NPM_TOKEN |
for npm |
none |
AUTOMATION_DOCKER_IMAGE |
for docker |
none; docker.io/owner/image |
AUTOMATION_DOCKER_CONTEXT |
no | . |
AUTOMATION_DOCKERFILE |
no | Dockerfile |
AUTOMATION_DOCKER_LATEST_TAG |
no | latest |
AUTOMATION_DOCKER_BUILD_ARGS |
no | additional shell-split docker build arguments |
REGISTRY_USER / REGISTRY_TOKEN |
for docker |
Docker Hub credentials |
AUTOMATION_PUBLISH_CHECK_COMMAND |
for command |
returns zero when already published |
AUTOMATION_PUBLISH_COMMAND |
for command |
repository-owned publish command |
AUTOMATION_RAILWAY_SERVICE |
for Railway | Railway service name or ID |
RAILWAY_TOKEN |
for Railway | project token scoped to the target environment |
AUTOMATION_DEPLOY_COMMAND |
for command deploy | repository-owned deploy command |
KNOT_DEPLOY_KEY, AUTOMATION_REPOSITORY, AUTOMATION_BOT_NAME, and
AUTOMATION_BOT_EMAIL are required by publishing because remote tags are its completion marker.
Publishing and deployment happen before tag creation, allowing a failed run to repair and retry
the incomplete release safely.
Both commands acquire short-lived Git-backed locks, so overlapping Spindle runs serialize instead
of moving a release branch, npm dist-tag, Docker moving tag, or deployment backward. The default
wait is 20 minutes and abandoned locks become replaceable after two hours; configure these with
AUTOMATION_LOCK_WAIT_SECONDS and AUTOMATION_LOCK_STALE_SECONDS if a release routinely runs
longer. Custom deployment commands must be idempotent because deployment has at-least-once
semantics when artifact publication succeeds but tag pushing must be retried.
The command adapter exports AUTOMATION_VERSION when used with git-cliff. Its check must
return 0 when that version is published and 1 when it is absent; any other status aborts the
release. The publish command runs only for status 1.
Examples #
examples/changesets-npmis the pnpm monorepo path.examples/git-cliff-docker-railwayis the commerce path.examples/git-cliff-commandshows a tag-driven Nix release plus a repository-owned Homebrew command.
Release lifecycle #
- A push to the default branch runs
tangled-release-pr. - If releasable changes exist, it rebuilds
chore/release, force-pushes it, and creates or updates the release PR round withtg. - Merging the release PR runs
tangled-publish. - New artifacts are published, optional deployment runs, and release tags are pushed.
- Subsequent non-release pushes are idempotent: existing artifacts and tags are skipped.
Bot setup #
Generate a dedicated key without a passphrase, register its public half on the bot account, and
store the private half as KNOT_DEPLOY_KEY:
ssh-keygen -t ed25519 -N "" -C "Tangled release bot" -f ./knot-deploy
tg ssh-key add ./knot-deploy.pub --account example.com --title "Spindle release bot"
rm ./knot-deploy ./knot-deploy.pub
Create an AT Protocol app password for the same account and store it as TG_APP_PASSWORD.
Development #
bash tests/test.sh
shellcheck -x scripts/*.sh scripts/lib/*.sh tests/test.sh
nix flake check
nix build .#release-pr
The repository's own Spindle runs these checks on Linux, which is also the environment used by
consumer workflows. Its test workflow consumes #release-pr and #publish as separate external
dependencies, verifying the same slim closures consumers receive.