Cut a release
The monthly minor from its release candidate a week before, a patch from the release branch, the signed release on git.torb.dev and in the extension stores, what the owner sets up once, and what to do when a job fails.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
A release of TorbScript is made by pushing a tag to the forge, git.torb.dev: v0.MINOR.PATCH for a stable release,
v0.MINOR.0-rc.N for a release candidate, on a commit of main or of the minor's release branch release/0.MINOR. The
workflow release checks the tag, runs every gate, and only when all of them are green publishes the archives, the
source, the portable seed, the VS Code extension and SHA256SUMS with its cosign signature as a release of
torbscript/language; release-sync then places them at https://torb.dev/download/<version>/, where the installers
read them. A stable release also goes to the Visual Studio Marketplace, Open VSX, Homebrew and Scoop, and every
nightly publishes its own extension to both stores too, as a pre-release with a store version of its own - VS Code's
"Switch to Pre-Release Version" always has main, without waiting for a release. A release candidate is a
pre-release: the forge marks it as one, release-sync lists it on the preview channel, the images get the tag
preview and not latest, and neither a store nor a package manager gets it. The calendar and its rules are
section 3 of the release record, the pipeline is
section 13, and the forge, its runners, the GitHub mirror and
the tokens are section 14.
Steps#
The calendar#
A minor every month on the first Tuesday, its release candidate on the Tuesday a week before, and a patch whenever the fix of a regression or of a security problem is ready. Before 1.0 only a minor may break, and every break ships the automatic migration the release notes name; a patch never breaks. The first releases:
| Release candidate | Stable release |
|---|---|
v0.1.0-rc.1, 2026-09-29 | v0.1.0, 2026-10-06 |
v0.2.0-rc.1, 2026-10-27 | v0.2.0, 2026-11-03 |
v0.3.0-rc.1, 2026-11-24 | v0.3.0, 2026-12-01 |
main always carries the number of the next minor, in four places: version = "0.2.0" in project.trb and in
compiler/project.trb, "version": "0.2.0" in editors/vscode/package.json - the extension carries the toolchain's
number - and languageVersion = "0.2.0" in compiler/src/package/version.trb, which torb --version prints and a
package's language line is compared with. A release candidate is a build of that number: the four places never carry
-rc.1. The workflow refuses a tag whose number without its suffix differs from any of them, and CI refuses a
package.json whose number is not project.trb's. The packages of the Agent Skills carry the number as well, and
nothing checks them: version: 0.2.0 in apm.yml and "version": "0.2.0" in skills/.codex-plugin/plugin.json,
whose number is how Codex tells a new plugin from the one it has cached.
The release candidate: the Tuesday before#
Before the very first candidate, once: the root server has to run a release-sync that knows the preview channel, and
serve the installers that do - the images of a nightly made after the channel was built (2026-09-29), with
TORB_VERSION=nightly and docker compose pull && docker compose up -d (Actions -> nightly -> Run workflow makes
one at once). An older release-sync lists 0.1.0-rc.1 as a stable release, points latest at it and sorts it above
0.1.0, and older installers never offer it.
Write the release notes in
docs/releases/0.2.0.md: every breaking change, and the command that migrates it (torb format,torb lint --fix). A candidate carries the notes of its number, and a filedocs/releases/0.2.0-rc.1.mdof its own where it needs one. Without either the release says one line, its targets and how to verify a download. What changed in the extension goes intoeditors/vscode/CHANGELOG.mdas a section## 0.2.0, which the stores show as its changelog.Wait for
cito be green onmainon the forge (Actions oftorbscript/language). The release runs the same gates again, but a redciis the cheaper place to find out.Branch
release/0.2and tag its first commit. Only the tag starts a release, and pushing it is the approval: the forge holds no job for a reviewer.$ git switch main $ git pull forgejo main $ git branch release/0.2 $ git push forgejo release/0.2 $ git tag v0.2.0-rc.1 $ git push forgejo v0.2.0-rc.1Move
mainto the next minor at once: the four places and the two Agent Skills packages say0.3.0in a commit of their own (chore(release): make 0.3.0 the next minor), andeditors/vscode/CHANGELOG.mdopens## 0.3.0 - unreleased. From here onrelease/0.2takes fixes only, andmainmoves on.Check the release page on git.torb.dev: a pre-release named
TorbScript 0.2.0-rc.1, onetorb-0.2.0-rc.1-<target>.tar.gzper tier 1 target - linux-x64, windows-x64, linux-arm64 and macos-arm64 - and a.zipfor Windows, the source, the seed with its.sha256,torbscript-0.2.0-rc.1.vsix(version 0.2.0 inside, marked as a pre-release),SHA256SUMSandSHA256SUMS.sig. The jobimagespushes the images tagged0.2.0-rc.1andpreview,publish-extensionsays that a pre-release stays out of the stores, andpublish-packagesis skipped. release-sync lists the candidate as0.2.0-rc.1 preview, and it installs withcurl -fsSL https://torb.dev/install.sh | TORB_INSTALL_CHANNEL=preview shortorb upgrade --channel preview.
The stable release: the first Tuesday#
Tag the commit of the last candidate again. Without a blocker the candidate becomes the release unchanged - the same commit, gated once more by the workflow:
$ git fetch forgejo --tags $ git tag v0.2.0 v0.2.0-rc.1^{} $ git push forgejo v0.2.0^{}names the commit of an annotated tag; for a lightweight one it is the same commit.Check the release page: the same assets, named
torb-0.2.0-<target>.tar.gzandtorbscript-0.2.0.vsix, a release and not a pre-release. The release is a draft until its last asset is uploaded, so the webhook reaches release-sync once, with everything there. The jobimagespushescr.torb.dev/torbscript/release-sync,siteandregistrytagged with the version andlatest, signed, andpublish-packagespushes the Homebrew formula and the Scoop manifest where the release has their archives. The root server takes the images withdocker compose pull && docker compose up -d.Check the extension in the stores. The job
publish-extensionpublishes the.vsixof the release to the Visual Studio Marketplace and to Open VSX; its log says "not published" for a store whose secret is not set. The Marketplace verifies an upload for a few minutes before its page shows the version; Open VSX shows it at once (open-vsx.org/extension/torbscript/torbscript).
A blocker: the next candidate#
A blocker is fixed on main first where the fix applies there, then brought to the release branch by a cherry-pick -
the history stays linear, and the branch never merges:
$ git switch release/0.2
$ git cherry-pick -x <commit of the fix>
$ git push forgejo release/0.2
$ git tag v0.2.0-rc.2
$ git push forgejo v0.2.0-rc.2ci runs on every push to release/*. The stable release is the last candidate, unchanged, tagged on the first
Tuesday; a blocker that is not fixed by then moves the date, never the rule that a stable release is a candidate that
was out first.
A patch: 0.2.1 from release/0.2#
Fixes only, never a break and never a feature: the fix of a regression or of a security problem, cherry-picked with
-xfrommainontorelease/0.2as above.The four places and the two Agent Skills packages say
0.2.1onrelease/0.2, inchore(release): 0.2.1, with the notes indocs/releases/0.2.1.md; cherry-pick the notes ontomainas well, so itsdocs/releases/lists every release.Tag and push; a patch has no candidate:
$ git tag v0.2.1 $ git push forgejo release/0.2 v0.2.1
In 0.x only the newest minor is maintained: once 0.3.0 is out, release/0.2 takes nothing more. From 1.0 the newest
minor gets every fix and the one before it security fixes for one more month. There is no long-term support release.
Which targets a release carries: all four tier 1 targets. linux-x64 is built on the forge's runner, and so is each
of windows-x64, linux-arm64 and macos-arm64 whose runner the repository variable TORB_RUNNERS names; the others are
cross-compiled on the linux-x64 runner by a pinned zig cc (the jobs cross-compiled on linux-x64 (<target>),
RELEASE.md section 13, "Cross-compiled archives"). A
cross-compiled binary is checked and started under wine (windows-x64) or qemu-user (linux-arm64) before anything is
published, macos-arm64's is checked and not started, and the release notes name every target that was cross-compiled.
None of them has passed the conformance suite on its machine: that is what the GitHub mirror does for each target's own
build after every push, and every morning its job download installs the newest nightly on real windows-x64,
linux-arm64 and macos-arm64 machines and runs it - look there before a release.
The nightly's extension is published the same way, automatically, by nightly.yml's own publish-extension: once
the nightly's release is out, its .vsix goes to both stores as a pre-release - vsce verify-pat torbscript and
ovsx verify-pat torbscript run first, so a wrong publisher or namespace fails with a clear message rather than an
obscure one from publish, and --skip-duplicate passes over a second nightly of the same day. Nothing to check by
hand unless a store's log says otherwise; the store version comes from tools/package-extension.sh
(MAJOR.(MINOR+1).YYYYMMDD), not from package.json.
Publishing a seed without a release is the forge's Actions -> seed -> Run workflow on main. It bootstraps on
linux-x64, runs tier A and tier B, and publishes the seed of main. That is what the second commit of a breaking change
waits for: the published seed has to understand the new form before a commit that uses it can bootstrap in CI or in a
fresh clone. The nightly publishes one every night that main changed, and tools/land.sh publish publishes the seed of
every landing.
The first seeds on the forge come from GitHub, where CI published them before the move, or from the main checkout:
$ GH_TOKEN=<GitHub token> TORB_FORGE_TOKEN=<forge token> sh tools/migrate-seeds.sh
$ sh tools/pack-seed.sh seed
$ TORB_FORGE_TOKEN=<forge token> sh tools/publish-seed.sh build/seed-archive/torb-seed-653af8bb6699.tar.gzPitfalls#
- A tag that fails the check is still a tag. The workflow publishes nothing, but the tag stays: delete it
(
git push --delete forgejo v0.2.0) before pushing the corrected one, and never reuse the number of a published release. A candidate is-rc.NofMAJOR.MINOR.0, from 1;-alpha.Nand-beta.Nexist only for a major (1.0.0-alpha.1); a tag of another shape -v0.2.0-preview.1,v0.2.1-rc.1- is refused or starts no release. - "is a commit of neither main nor release/0.2": the tag is on a commit of another branch. A candidate and a patch
are tagged on
release/0.2, pushed before the tag. - A release of
release/0.2publishes no seed intoseeds.txt, and says so: every clone ofmainbootstraps from the newest seed there, and the release branch stays behindmain. Its seed is an asset of the release. The other way round, CI ofrelease/0.2bootstraps frommain's newest seed, somainkeeps reading every form of the languagerelease/0.2uses until0.3.0is out: a form is removed from the compiler only after that. - A release job that fails after the gates - the signature, the token, the upload - left a draft release or none at all. A draft has no tag yet and no webhook was sent: delete the draft on the release page, then re-run the failed jobs of the same run; the gates are not run again.
- "the repository variable TORB_PUBLISH_AUDIENCE is not set" or "the secret COSIGN_PRIVATE_KEY is not set": a step of "One-time setup" below is missing. A token the forge refuses (401) means the Authorized Integration's rules do not admit this run - another workflow file, ref or event than the ones it names.
- A job that waits for ever has a
runs-onlabel no online runner has:TORB_RUNNERSnames a target whose runner is gone. Remove the target from the variable, or bring the runner back. - A job
cross-compiled on linux-x64 (<target>)is red, and nothing was published. "no source had zig-x86_64-linux-...": neither a mirror of Zig nor ziglang.org answered with the pinned archive - re-run the job. A red check oftools/cross.sh(a DLL, a dynamic binary, no signature) is a real finding about the build; a red smoke test under wine or qemu-user is one too until it is shown to be the emulator's - then the same smoke test of that target's native build on the GitHub mirror decides. A new Zig is a new pin intools/fetch-zig.sh: the version and the four SHA-256 ofhttps://ziglang.org/download/index.json, with each archive's minisign signature checked against the key on ziglang.org/download. - Forgejo ignores a job that has a
permissions:key ("not supported, the job is ignored"). The workflows of.forgejo/have none; a write goes through an Authorized Integration (.forgejo/actions/token). - A release needs a
LICENSE. The check refuses a release without one. fetch-seed.shanswers "no index had the seed": neither the forge nor GitHub lists it. The forge's index is public; GitHub's needsGH_TOKENwhile that repository is private.- A breaking change pushed in one go fails CI at the bootstrap: the published seed does not know the new form. Push
the teaching commit, run
seed, then push the migration. publish-extensionfailed after the release or the nightly is out: the release stays as it is. A store that refused a token (401, "Access Denied", or an expired token - an Azure DevOps token lives a year at most) needs a new token in its secret (step 9 of "One-time setup"); then re-run that job alone, which passes over the store that has the version already. Both stores keep a published version for good, so the fix of a release's broken extension is the next patch release - a nightly's fixes itself the next night, with a store version of its own.- The job
extensionof CI is red with "package.json says version ... and project.trb ...": the number went into the manifests but not intoeditors/vscode/package.json("The calendar" names the four places). - "says languageVersion ...":
compiler/src/package/version.trbwas left behind. It is a constant of the compiler, so moving it bootstraps once, like any change ofcompiler/src.
Full example#
The minor 0.2.0, from a green main: its candidate on 2026-10-27, and the release on 2026-11-03.
$ git switch main
$ git pull forgejo main
$ grep -E '^version|^ "version"|languageVersion =' project.trb compiler/project.trb editors/vscode/package.json \
compiler/src/package/version.trb
project.trb:version = "0.2.0"
compiler/project.trb:version = "0.2.0"
editors/vscode/package.json: "version": "0.2.0",
compiler/src/package/version.trb:public const languageVersion = "0.2.0"
$ git branch release/0.2
$ git push forgejo release/0.2
$ git tag v0.2.0-rc.1
$ git push forgejo v0.2.0-rc.1A week later, without a blocker:
$ git fetch forgejo --tags
$ git tag v0.2.0 v0.2.0-rc.1^{}
$ git push forgejo v0.2.0Verifying the download afterwards, from a directory with the archive, SHA256SUMS and SHA256SUMS.sig:
$ cosign verify-blob --key https://git.torb.dev/torbscript/language/raw/branch/main/tools/deploy/cosign.pub \
--signature SHA256SUMS.sig --insecure-ignore-tlog=true SHA256SUMS
Verified OK
$ sha256sum --check --ignore-missing SHA256SUMS
torb-0.2.0-linux-x64.tar.gz: OKA fresh clone bootstraps from the newest published seed without any setup beyond a C compiler:
$ sh tools/bootstrap.sh
no seed on this machine: fetching the newest published seed (sh tools/fetch-seed.sh)One-time setup#
Everything below is done once, by the owner, outside this repository. Where a value is written here, it is the exact value to enter.
The forge (RELEASE.md section 14): git.torb.dev, repository
torbscript/language, one runner torb-runner-1 with the labels ubuntu-latest and ubuntu-24.04.
Two Authorized Integrations, created by a user who is an owner of the organization
torbscript- a token of an integration acts as that user, limited to the integration's permissions. User settings -> Authorized Integrations -> Add authorized integration -> Forgejo Actions (Local):Field torbscript-publishtorbscript-imagesSelect repository torbscript/languagetorbscript/languageWorkflow file {release,nightly,seed,benchmarks}.yml{release,nightly,images}.ymlGit reference {refs/heads/main,refs/tags/v*}{refs/heads/main,refs/tags/v*}Event push, schedule, workflow_dispatch push, schedule, workflow_dispatch Repository and organization access Specific repositories: torbscript/language,torbscript/homebrew-tap,torbscript/scoop-bucketPublic only Permissions repository: Read and write; everything else: No access package: Read and write; everything else: No access The forge shows each integration's audience once it is saved (
u:<id>:<uuid>); it is not a secret. An integration limited to specific repositories can only be given the repository and issue permissions, which is why the images need a second one.benchmarks.ymlis in the first rule for a run of it by hand (Actions -> benchmarks -> Run workflow): called bynightly.yml, its job carriesnightly.yml's name, and an integration saved before the rule named it publishes the nightly's numbers all the same and refuses only the run by hand.Repository variables (
torbscript/language-> Settings -> Actions -> Variables):TORB_PUBLISH_AUDIENCE= the audience oftorbscript-publish,TORB_IMAGES_AUDIENCE= the audience oftorbscript-images, and - once a runner of another target exists -TORB_RUNNERS= the targets that have one, separated by spaces:windows-x64 linux-arm64 macos-arm64or any of them.The signing key, on a machine the owner trusts, with cosign 2.4.1:
cosign generate-key-pairasks for a password and writescosign.keyandcosign.pub. Repository secrets (Settings -> Actions -> Secrets):COSIGN_PRIVATE_KEY= the whole content ofcosign.key,COSIGN_PASSWORD= the password. Commitcosign.pubastools/deploy/cosign.pub, copy it besidecompose.ymlon the root server, and keepcosign.keyand its password offline as well - without them no release can be signed with the same key.Protection: Settings -> Branches: protect
mainandrelease/*against force pushes and deletion; Settings -> Tags: protectv*andseedsso that only the owner - and the integration, which acts as the owner - creates them.More runners, when there are machines for them: a runner registered with the label of its target (
windows-x64,linux-arm64,macos-arm64), and the target added toTORB_RUNNERS. A job for a label no runner has would wait, which is why the workflows run those jobs only for the targets the variable names. A target the variable names is no longer cross-compiled: its release binary is built and gated on its runner.The package manager channels: create
torbscript/homebrew-tapandtorbscript/scoop-bucketon the forge, each with a first commit that holds an emptyFormula/orbucket/directory (a.gitkeep), and give each a push mirror togithub.com/TorbScript/homebrew-tapandgithub.com/TorbScript/scoop-bucket(step 7):brew tap torbscript/tapfinds a tap on GitHub without a URL. winget's first manifest is submitted by hand from Windows,wingetcreate new https://torb.dev/download/<version>/torb-<version>-windows-x64.zip(publisherTorbScript, packageTorb, portable, the nestedbin/torb.exe); every later version with the command the release job prints.GitHub as the mirror: on GitHub, a fine-grained personal access token limited to
TorbScript/languagewith Contents: Read and write and Workflows: Read and write (without the second, GitHub refuses a push that changes.github/workflows/). On the forge,torbscript/language-> Settings -> Repository -> Mirror settings -> Push mirror: Git remote repository URLhttps://github.com/TorbScript/language.git, Username the GitHub account, Password the token, Sync when commits are pushed on, interval8h0m0s. The same for the tap and the bucket with tokens of their repositories. MakingTorbScript/languagepublic turns.github/workflows/portable.ymlon; while it is private that workflow does nothing.Once the forge has every seed (
sh tools/migrate-seeds.shanswers "nothing to copy"): the releaseseedson GitHub may stay as an archive; nothing reads it any more.The stores of the VS Code extension, so that
publish-extensionof a release, and of every nightly, publishestorbscript.torbscript. Until both secrets exist the job skips the store without one and says so; nothing else waits for them.The Visual Studio Marketplace, with one Microsoft account for all three steps:
- The token: sign in at
https://dev.azure.com(create an Azure DevOps organization when asked - any name, it only holds the token), then User settings (the icon beside the avatar, top right) -> Personal access tokens -> New Token. Nametorbscript-vsce; Organization All accessible organizations (a token of one organization is refused by the Marketplace); Expiration: the longest offered, one year - note the date, a release after it fails until the token is renewed; Scopes: Custom defined -> Show all scopes -> Marketplace: Manage. Create, and copy the token: it is shown once. - The publisher:
https://marketplace.visualstudio.com/manage-> Create publisher: IDtorbscript(it cannot be changed, and it is the first half oftorbscript.torbscript), NameTorbScript. Optionally, the publisher's "Verified domain" withtorb.dev(a TXT record) gives the store page its verified mark. - The check, on any machine with Node.js 22:
VSCE_PAT=<token> npx @vscode/vsce@4.0.0 verify-pat torbscriptanswers that the token may publish astorbscript.
Open VSX (the store of VSCodium, Cursor, Gitpod and the other editors built on VS Code's open source):
- Sign in at
https://open-vsx.orgwith a GitHub account, and sign the Eclipse Foundation's Open VSX Publisher Agreement under Profile (it asks for an Eclipse account, which can be created there). - The token: Profile -> Access Tokens -> Generate New Token, description
torbscript release; copy it, it is shown once. It does not expire. - The namespace:
OVSX_PAT=<token> npx ovsx@1.2.0 create-namespace torbscriptcreatestorbscript, owned by that account. To have it verified (the store's mark that the namespace belongs to the project), open an issue athttps://github.com/EclipseFdn/open-vsx.org/issuesasking for ownership of the namespacetorbscript.
The secrets:
torbscript/language-> Settings -> Actions -> Secrets -> Add secret:VSCE_PAT= the Azure DevOps token,OVSX_PAT= the Open VSX token. Onlypublish-extensionofreleaseand ofnightlyreads them. A renewed token replaces the value of its secret; nothing else changes.- The token: sign in at
The root server (RELEASE.md section 7.11): torb.dev with the downloads, packages.torb.dev with the registry, from the images every release pushes to cr.torb.dev, behind the Traefik that already runs on the machine - beside the forge's own stack or in it. Nothing is built on the server.
- DNS:
torb.dev,www.torb.devandpackages.torb.devpointed at the machine (or at the CDN in front of it). - The files: a directory such as
/opt/torbwithtools/deploy/compose.example.ymlcopied ascompose.yml,.env.exampleas.env(never committed),cosign.pub,litestream.ymlandbackup.shbeside them. The data directories belowTORB_DATA(default/srv/torb) belong to the containers' user:mkdir -p /srv/torb/download /srv/torb/registry /srv/torb/database /srv/torb/index-mirror && chown -R 1000:1000 /srv/torb. No runner of the forge ever mounts any of them. - The
.env:TRAEFIK_NETWORK,TRAEFIK_ENTRYPOINTandTRAEFIK_CERTRESOLVER: the Docker network the existing Traefik watches, its HTTPS entrypoint and its Let's Encrypt resolver, as its own configuration names them.RELEASE_SYNC_FORGE=forgejo,RELEASE_SYNC_URL=https://git.torb.dev,RELEASE_SYNC_REPOSITORY=torbscript/language(the defaults),RELEASE_SYNC_TOKENempty - the repository is public - andRELEASE_SYNC_WEBHOOK_SECRETany random string (openssl rand -hex 32), entered in the webhook too.REGISTRY_ADMINISTRATOR_TOKEN: another random string, for step 6; empty again afterwards.REGISTRY_FORGEJO_ISSUERS=https://git.torb.dev/api/actions: whose Forgejo Actions may publish packages.REGISTRY_SIGNING_KEY: the seed of the key every record of the index is signed with, whichdocker compose run --rm registry signing-keyprints together with its public key (after step 4's pull). Keep a copy of the seed off the machine: a lost key cannot endorse its successor, and every client that trusts it refuses an index signed by another. The public key goes intoshippedKeysofcompiler/src/package/registry.trbin the release that opens the registry (RELEASE.md section 7.14). Without the variable the registry signs with a development key it makes itself, and says so in its log.BACKUP_ENDPOINT,BACKUP_REGION,BACKUP_BUCKET,BACKUP_ACCESS_KEY_ID,BACKUP_SECRET_ACCESS_KEY: a bucket of S3-compatible object storage at another provider than the server's, and a key limited to it;RESTIC_PASSWORD: a random string kept somewhere else too - without it the file backups cannot be read.
- Start:
docker compose pull && docker compose up -d; the images of cr.torb.dev are public, nodocker login. An update is the same command;TORB_VERSIONpins a version,latestfollows stable releases,previewthe release candidates,nightlythe nightlies. While there is no stable release,latestdoes not exist yet:previewornightlyuntil 2026-10-06.docker compose logs <service>says what each one does. The site alone hasedgeas well, the site ofmainwithin minutes of a push ("The site of main" below). - The webhook: on the forge,
torbscript/language-> Settings -> Webhooks -> Add webhook -> Forgejo: Target URLhttps://torb.dev/webhook, HTTP methodPOST, POST content typeapplication/json, Secret the value ofRELEASE_SYNC_WEBHOOK_SECRET, Trigger on: Custom events -> Releases, Active. The forge refuses to deliver to a private address ([webhook] ALLOWED_HOST_LIST,externalby default): where the forge resolvestorb.devto one - the same machine behind a hairpin - addtorb.devto that list. Verify it with a release, or with the periodic check, which runs when the container starts and everyRELEASE_SYNC_POLL_SECONDS(default 300) and places the newest stable release and the nightlies of the last thirty days that are missing - the forge never delivers a failed webhook again - and look for/srv/torb/download/<version>/;docker compose logs release-syncsays why one is missing - most often a signature that does not verify againstcosign.pub, which is by design.tools/release-sync/README.mdhas every variable and the answers of the webhook. - The first accounts: with
REGISTRY_ADMINISTRATOR_TOKENset,curl -X POST https://packages.torb.dev/api/1/accounts -H "Authorization: Bearer $ADMIN" -d '{"name":"torben"}'answers the account's first token (shown once - keep it; it lives 30 days, and once the account has an authenticator app every call below that creates or changes owners also sends its code or a recovery code as-H "Torb-Code: 123456"; with a passkey only, start a step-up -curl -X POST .../api/1/step-up -H "Authorization: Bearer $TOKEN" -d '{"call":"owners"}'("mint"for a token) - type itsuserCodeat.../accountand touch the passkey, then send-H "Torb-Code: step-up:<secret>"once, on one call of that class, within five minutes); an organisation iscurl -X POST .../api/1/owners -H "Authorization: Bearer $TOKEN" -d '{"name":"acme"}', and the reserved ownertorbscriptonly with the operator's token and"account":"torben". Then hand the account's person a claim link -curl -X POST .../api/1/accounts/torben/claim -H "Authorization: Bearer $ADMIN"answers itsurl, shown once and working once within the hour; deliver it at once, and keep query strings out of the proxy's access log - so they sign in through a provider, which links that identity to the account, and enrol an authenticator app or a passkey on the account page; from then on they mint their own tokens (and revoke the operator's there), and the operator makes none for the account while it has no factor. Then emptyREGISTRY_ADMINISTRATOR_TOKENanddocker compose up -dagain.tools/registry/README.mdlists every call. - Trusted publishing for a package, so its CI publishes without a stored token: an owner of it sends
curl -X PUT https://packages.torb.dev/api/1/packages/acme/http/trusted-publishers -H "Authorization: Bearer $TOKEN" -d '{"publishers":[{"repository":"git.torb.dev/acme/http","workflow":"release.yml","ref":"refs/tags/v*"}],"required":true}'for a repository on the forge - its job hasenable-openid-connect: trueand runstorb publish- or{"repository":"acme/http","workflow":"release.yml","environment":"release"}for one on GitHub, whose job haspermissions: id-token: write.requiredswitches tokens off for the package. With a second factor on the account, add-H "Torb-Code: ..."(a step-up, for a passkey: step 6). - The index mirror (optional): an empty public repository on the forge, e.g.
torbscript/index, cloned into/srv/torb/index-mirrorwith a remote whose URL carries an access token of the forge with repository write access to that repository alone (git clone https://<user>:<token>@git.torb.dev/torbscript/index.git /srv/torb/index-mirror, thenchown -R 1000:1000it), and in.envREGISTRY_MIRROR=/srv/torb/index-mirrorandREGISTRY_MIRROR_PUSH=1. - Backups: Litestream runs as a service and replicates the database at once. The files are backed up by the
host's cron:
17 3 * * * cd /opt/torb && docker compose --profile backup run --rm backup >> /var/log/torb-backup.log 2>&1. Restoring the database isdocker compose run --rm litestream restore -config /etc/litestream.yml /srv/torb/database/registry.dbbefore the registry starts; the files come back withrestic restore latest --target /fromdocker compose --profile backup run --rm --entrypoint sh backup. - The CDN (optional, later): in front of both host names, honouring the
Cache-Controlthe site sends - a year for an archive, a minute for an index file. - Changing the signing key:
registry signing-keymakes the next one; its public key goes intoREGISTRY_SIGNING_KEY_SUCCESSORanddocker compose up -dannounces it inindex/config.trb, endorsed by the current key, and intoshippedKeysof the next release. Once that release is out, the new seed goes intoREGISTRY_SIGNING_KEYand the successor variable is emptied. Replacing the key without announcing it makes every client that trusted the old one refuse the index.
Verifying an image by hand:
$ cosign verify --key https://git.torb.dev/torbscript/language/raw/branch/main/tools/deploy/cosign.pub \
--insecure-ignore-tlog=true cr.torb.dev/torbscript/registry:0.2.0The site of main#
Every push to main that changes what the site is built from - docs/, brand/, playground/ with its editor,
std/ (the reference), the generators in compiler/src/documentation/, compiler/src/reference/ and
compiler/src/highlight/, the installers, tools/deploy/Dockerfile.site, nginx.conf, cosign.pub or
images.yml itself - builds cr.torb.dev/torbscript/site from that commit and pushes it, signed like every image,
without waiting for a nightly (RELEASE.md section 7.11).
It is the job edge of images.yml, and it runs no gate: main moves only through landings whose gates were green,
and ci runs them again beside it.
| Tag | Moves | What it is |
|---|---|---|
edge | with every such push | the newest site of main |
main-<commit>-<time> | never | the site of one commit: its first eight hexadecimal digits and its committer time in seconds, e.g. main-3b6b8355-1790707145 - later commits of main sort higher |
Neither touches latest, nightly or preview. The documentation in it is /docs/main/, and /docs/latest/ leads
there. A push that changes only the rest of the compiler builds nothing: the playground of edge stays as it was until
the next push that does, or until Actions -> images -> Run workflow on main with the channel edge, which builds
the head of main as well. From a push to a pushed image takes one and a half to two minutes when only the site's
own files changed, five to eight when std/ or a generator did (the compiler comes from the seed that the landing
published, and the playground is compiled again), and up to a quarter of an hour when there was no such seed - a
push that did not go through tools/land.sh publish. A newer push cancels the run it supersedes.
Following it with Flux (the cluster and its repository are the owner's): the image automation of Flux selects the
newest main-* tag by the number at its end, and the setter writes it into the manifest that runs the site.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: torb-site
namespace: flux-system
spec:
image: cr.torb.dev/torbscript/site
interval: 1m
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: torb-site
namespace: flux-system
spec:
imageRepositoryRef:
name: torb-site
filterTags:
pattern: '^main-[0-9a-f]{8}-(?P<time>[0-9]+)$'
extract: '$time'
policy:
numerical:
order: ascThe manifest that runs the site marks its image for the setter, and an ImageUpdateAutomation with
strategy: Setters commits each newer tag into it:
containers:
- name: site
image: cr.torb.dev/torbscript/site:main-3b6b8355-1790707145 # {"$imagepolicy": "flux-system:torb-site"}The images are public, so the ImageRepository needs no secret. Rolling back is pinning an older main-* tag.
Following edge instead works too - an ImagePolicy whose filterTags.pattern is '^edge$', with
digestReflectionPolicy: Always and an interval, and the setter writes edge@sha256:... - but the tag alone then
never says which commit runs. Every such push adds a main-* tag to cr.torb.dev; a cleanup rule of the owner's
container packages (torbscript -> Settings -> Packages) that removes the versions matching main-.* beyond the
newest few keeps them from piling up. Verifying one:
$ cosign verify --key https://git.torb.dev/torbscript/language/raw/branch/main/tools/deploy/cosign.pub \
--insecure-ignore-tlog=true cr.torb.dev/torbscript/site:edgeRelated#
- The release record - versions, channels, what a download contains, section 13, the pipeline as built, and section 14, the forge.
- Working on the compiler - the gates, the seed and the two-commit rule.
- The docs commands - the gates this page itself passes.