From 3d60951c8d973d7c4596b9b249a89f5c00d9fb7b Mon Sep 17 00:00:00 2001 From: Florian-mfr Date: Sat, 10 Oct 2026 14:50:55 +0200 Subject: [PATCH 1/3] chore(release): server.json follows the version release.sh sets the version of server.json, and of its npm package, with the package.json files: the workflow already publishes it at the version of the tag, and the file in the repository now says the same. --- scripts/README.md | 4 ++-- scripts/release.sh | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index f8503ec..b2031a7 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -94,8 +94,8 @@ scripts/release.sh 2.1.0-rc.1 # a prerelease: only the tag Write the `CHANGELOG.md` section of the version first: the release notes come from it. Bump the digests of `POSTGRES_IMAGE` and `TRAEFIK_IMAGE` in `install.sh` too (`docker buildx imagetools inspect postgres:17-alpine` gives -the digest of the index), since nothing updates them. The script sets the version of every `package.json` and the default -version of `install.sh`, commits, and makes an annotated tag. The tag must be +the digest of the index), since nothing updates them. The script sets the version of every `package.json`, of `server.json` and the +default version of `install.sh`, commits, and makes an annotated tag. The tag must be on a commit of `master`: merge the branch with a merge commit (squash and rebase rewrite the tagged commit), then push the tag. It starts `.github/workflows/release.yml`, which refuses a tag that is not on `master`, diff --git a/scripts/release.sh b/scripts/release.sh index 0127fde..2089dfe 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # -# Prepares a release: sets the version of every package.json and the -# default version of install.sh, commits, and tags. Pushing the tag starts +# Prepares a release: sets the version of every package.json, of server.json +# and the default version of install.sh, commits, and tags. Pushing the tag starts # the Release workflow (.github/workflows/release.yml), which publishes the # images, the CLI and the GitHub release. The workflow refuses a tag whose # commit is not on master: merge the branch into master with a merge commit @@ -34,7 +34,17 @@ if [[ "$version" != *-* ]]; then ' "$file" "$version" done sed -i.bak "s/^DEFAULT_VERSION=\"[^\"]*\"$/DEFAULT_VERSION=\"$version\"/" install.sh && rm install.sh.bak - git add package.json apps/*/package.json packages/*/package.json install.sh + # The MCP Registry's entry: the workflow publishes it at the version of the tag, this keeps the file in step. + # shellcheck disable=SC2016 # JavaScript, not shell + node -e ' + const fs = require("fs"); + const version = process.argv[1]; + const server = JSON.parse(fs.readFileSync("server.json", "utf8")); + server.version = version; + for (const pkg of server.packages) pkg.version = version; + fs.writeFileSync("server.json", `${JSON.stringify(server, null, 2)}\n`); + ' "$version" + git add package.json apps/*/package.json packages/*/package.json install.sh server.json git diff --cached --quiet || git commit -q -m "chore(release): $version" fi From 086c96399b61b3887a1a553c81514987a3144704 Mon Sep 17 00:00:00 2001 From: Florian-mfr Date: Sat, 10 Oct 2026 14:50:55 +0200 Subject: [PATCH 2/3] docs: the changelog of 2.2.0 --- CHANGELOG.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f45cfe6..86429b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,21 @@ # Changelog -## Unreleased +## 2.2.0 +A harder Spawner: environments kept from the cloud's metadata and the server's services, source repositories chosen by admins, Traefik 3.7 and releases you can verify. And its documentation, now on https://spawner.run. + +- **Environments no longer reach the cloud's metadata or the server's services**: the code of a branch could read the credentials of the server from the metadata service of its cloud (AWS, GCP, Azure, Hetzner, OVH...), or call a service listening on the server. A new `spawner-firewall` container loads rules for every container of Docker's bridge networks: the metadata addresses are refused, but for DNS, and the server answers on DNS, HTTP and HTTPS only. The installer also has Docker load them before it starts, so that they hold from boot on: after an update from the dashboard, run `install.sh --upgrade` once on the server for that part. +- **Source repositories are chosen by admins**: the other sources of `spawner.yaml` must come from repositories listed in the project (Projects, Edit, "Source repositories"); `spawner up` refuses the others before sending anything. A branch could otherwise make Spawner clone any repository its deploy keys reach, or an address of the internal network. The upgrade lists for each project the repositories its live environments already use. +- **Traefik 3.7**: Traefik 3.5, which no longer gets security fixes, gives way to 3.7; upgrading Spawner pulls it. Traefik 3.7 embeds lego v5, which dropped the DNS providers azure (use azuredns), dnspod, googledomains, cloudxns, brandit, iwantmyname and iij (use iijdpf): a server set up with one of them switches before upgrading. Request headers whose name holds a character other than a letter, a digit or a dash (`X_Spawner_Preview`, `X.Forwarded.For`) no longer reach applications, so an application that reads headers the PHP or CGI way cannot be handed one that Traefik removes or sets. +- **What you download can be verified**: the images, `install.sh` and the CLI bundle of a release carry build provenance attestations (`gh attestation verify install.sh -R Flosk6/Spawner`). The `install.sh` of a release names the digest of its image, and updates from the dashboard read it there: both run the image the release built, whatever its tag points to later. Postgres and Traefik are pinned by digest. +- **Terminals and log streams close when their access goes**: deactivating a user, changing their role or revoking a token now closes the terminals and the streams of logs they had open, at once, and an expired token's within 30 seconds. +- **Uploads stop at their first limit**: an archive is read as a stream that stops at the first file, byte or decompressed byte over the limits, so that a compression bomb costs no more than a legitimate archive; a person has 5 deploys of uploaded code waiting to start at most. +- **A harder installation**: + - The installer no longer makes a token of the installation with every scope (`SPAWNER_BOOTSTRAP_TOKEN`) unless given one; installations that have one keep it until you empty its line in `.env`. + - It refuses a version older than the installed one (its database only migrates forward; `--allow-downgrade` after restoring a backup), a data directory other than the installation's, and system directories; it never makes new secrets over an installation that lost them, never upgrades without a backup (it starts Postgres for it), writes `.env` and `dns.env` in one move, keeps a trailing `=` of their values, and no longer shows the DNS credentials typed. + - The data directory is mode 700, rendered compose files and logs are readable by Spawner only, Traefik runs without capabilities but binding ports, Postgres and Traefik have memory limits, Spawner's code belongs to root, and Prisma's telemetry is off. A local install over HTTP warns that it serves every interface; the development stack listens on 127.0.0.1. + - git reaches GitHub, GitLab and Bitbucket with the host keys they publish, even the first time. +- **The installer** warns below 4 GiB of memory, where no environment starts with the default settings, and names the settings to lower. It accepts an `/opt/spawner` holding only `.env` and `dns.env`, as when restoring on a new server. - **Smaller hardening**: - Previews: a cookie a preview sets under the name of Spawner's no longer locks its visitors out; share links opened on public URLs never reach the application; the waiting page of a public URL no longer says why its environment failed; logging out takes the preview cookie off the browser; the cache of preview hosts is bounded. - The dashboard's page has a Content-Security-Policy that runs its own scripts only; every answer carries `Referrer-Policy`, and over HTTPS `Strict-Transport-Security`. @@ -9,23 +23,13 @@ - Compose: an environment has 4 CPUs and 4096 processes to share between its services (`SPAWNER_ENV_CPUS`, `SPAWNER_ENV_PIDS`), as it has its memory, and no service gets raw sockets (`NET_RAW`). A file asking more CPUs or processes than that is refused. - Uploads: links are followed through the links they lead to once extracted. - CLI: `spawner login` opens http and https URLs only; untracked `.env-*`, `.npmrc`, private keys and `*.pem` files stay home like `.env` files; a Dockerfile is read only when it is a regular file of 1 MiB at most; the MCP server fences what comes from the environment and tells the model to read it as data. -- **A harder installation**: - - The installer no longer makes a token of the installation with every scope (`SPAWNER_BOOTSTRAP_TOKEN`) unless given one; installations that have one keep it until you empty its line in `.env`. - - It refuses a version older than the installed one (its database only migrates forward; `--allow-downgrade` after restoring a backup), a data directory other than the installation's, and system directories; it never makes new secrets over an installation that lost them, never upgrades without a backup (it starts Postgres for it), writes `.env` and `dns.env` in one move, keeps a trailing `=` of their values, and no longer shows the DNS credentials typed. - - The data directory is mode 700, rendered compose files and logs are readable by Spawner only, Traefik runs without capabilities but binding ports, Postgres and Traefik have memory limits, Spawner's code belongs to root, and Prisma's telemetry is off. A local install over HTTP warns that it serves every interface; the development stack listens on 127.0.0.1. - - git reaches GitHub, GitLab and Bitbucket with the host keys they publish, even the first time. -- **Source repositories are chosen by admins**: the other sources of `spawner.yaml` must come from repositories listed in the project (Projects, Edit, "Source repositories"); `spawner up` refuses the others before sending anything. A branch could otherwise make Spawner clone any repository its deploy keys reach, or an address of the internal network. The upgrade lists for each project the repositories its live environments already use. -- **Terminals and log streams close when their access goes**: deactivating a user, changing their role or revoking a token now closes the terminals and the streams of logs they had open, at once, and an expired token's within 30 seconds. -- **Uploads stop at their first limit**: an archive is read as a stream that stops at the first file, byte or decompressed byte over the limits, so that a compression bomb costs no more than a legitimate archive; a person has 5 deploys of uploaded code waiting to start at most. -- **Environments no longer reach the cloud's metadata or the server's services**: the code of a branch could read the credentials of the server from the metadata service of its cloud (AWS, GCP, Azure, Hetzner, OVH...), or call a service listening on the server. A new `spawner-firewall` container loads rules for every container of Docker's bridge networks: the metadata addresses are refused, but for DNS, and the server answers on DNS, HTTP and HTTPS only. The installer also has Docker load them before it starts, so that they hold from boot on: after an update from the dashboard, run `install.sh --upgrade` once on the server for that part. -- **What you download can be verified**: the images, `install.sh` and the CLI bundle of a release carry build provenance attestations (`gh attestation verify install.sh -R Flosk6/Spawner`). The `install.sh` of a release names the digest of its image, and updates from the dashboard read it there: both run the image the release built, whatever its tag points to later. Postgres and Traefik are pinned by digest. -- **Traefik 3.7**: Traefik 3.5, which no longer gets security fixes, gives way to 3.7; upgrading Spawner pulls it. Traefik 3.7 embeds lego v5, which dropped the DNS providers azure (use azuredns), dnspod, googledomains, cloudxns, brandit, iwantmyname and iij (use iijdpf): a server set up with one of them switches before upgrading. Request headers whose name holds a character other than a letter, a digit or a dash (`X_Spawner_Preview`, `X.Forwarded.For`) no longer reach applications, so an application that reads headers the PHP or CGI way cannot be handed one that Traefik removes or sets. -- **The installer** warns below 4 GiB of memory, where no environment starts with the default settings, and names the settings to lower. It accepts an `/opt/spawner` holding only `.env` and `dns.env`, as when restoring on a new server. - **The documentation moves to https://spawner.run**, with new pages: a quickstart from a fresh server to a first environment; previews for pull requests from GitHub Actions and GitLab CI; the two example projects; troubleshooting. Also new: every key of `spawner.yaml` and the codes of a refused `.spawner/`, what an application needs to run behind Spawner, every option, variable and error code of the CLI and what each MCP tool takes and returns, VS Code with GitHub Copilot, Gemini CLI and Devin Desktop for agents, and what Spawner does not do. - **Corrections to the documentation**: the minimum memory is 4 GiB, not 2; the compose policy listed `platform` under `build` as allowed, which Spawner refuses; services without `mem_limit` share what is left of the environment's memory, up to 512 MiB each; public URLs (`auth: none`) never wake a sleeping environment; with GitHub login set to an organization, its members get an account at their first login, without an invitation; `admin.js invite` links last 24 hours; the Dockerfile example of "Dockerfiles that share their layers" builds again; removing Spawner and the local install download the installer from the release, as upgrades do. - `spawner init` links `spawner.yaml` to https://spawner.run/docs/manifest/ instead of an outdated branch of the repository, and the instructions it writes for coding agents say what to do on exit code 6. The dashboard's Docs button opens https://spawner.run/docs/. `spawner login` says a 90-day token expires in 90d, not 89d 24h. - The MCP server is listed on the official MCP Registry as `io.github.Flosk6/spawner`, and spawner-cli on npm links to https://spawner.run. Release pages say what Spawner is, give the install and upgrade commands apart, and show how to check the files against `SHA256SUMS`. +Upgrading from 2.1.0: update from the dashboard or with `install.sh --upgrade`, then run `install.sh --upgrade` once on the server, so that Docker loads the firewall rules of the environments before it starts any container (an update from the dashboard installs the firewall container, not Docker's part). Before upgrading, a server set up with a DNS provider that lego v5 dropped switches to another one (see Traefik 3.7 above). The upgrade lists, for each project, the repositories its live environments already take sources from; new ones need an admin. An installation from 2.1 or before keeps its bootstrap token until you empty its line in `/opt/spawner/.env`. + ## 2.1.0 A new dashboard, and patched dependencies. From ff29444fb3aff991c8d9bd1aed2ba83128e7b6cf Mon Sep 17 00:00:00 2001 From: Florian-mfr Date: Sat, 10 Oct 2026 14:50:56 +0200 Subject: [PATCH 3/3] chore(release): 2.2.0 --- apps/api/package.json | 2 +- apps/cli/package.json | 2 +- apps/web/package.json | 2 +- install.sh | 2 +- package.json | 2 +- packages/core/package.json | 2 +- packages/types/package.json | 2 +- packages/utils/package.json | 2 +- server.json | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 433ccd3..c728e57 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@spawner/api", - "version": "2.1.0", + "version": "2.2.0", "private": true, "description": "Spawner API - Environment management system", "main": "dist/main.js", diff --git a/apps/cli/package.json b/apps/cli/package.json index c9e9160..907a67f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "@spawner/cli", - "version": "2.1.0", + "version": "2.2.0", "private": true, "description": "Spawner CLI and MCP server: preview environments from a worktree or a branch", "license": "Apache-2.0", diff --git a/apps/web/package.json b/apps/web/package.json index 7e3d89d..84d4a27 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@spawner/web", - "version": "2.1.0", + "version": "2.2.0", "private": true, "license": "Apache-2.0", "type": "module", diff --git a/install.sh b/install.sh index ad80ed2..42fb680 100755 --- a/install.sh +++ b/install.sh @@ -43,7 +43,7 @@ set -euo pipefail -DEFAULT_VERSION="2.1.0" +DEFAULT_VERSION="2.2.0" # The digest of the image of DEFAULT_VERSION, which the release workflow # writes into the install.sh it publishes: installing or upgrading to that # version runs the image the release built, whatever its tag points to later. diff --git a/package.json b/package.json index 7751d63..7b8e2b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spawner-monorepo", - "version": "2.1.0", + "version": "2.2.0", "private": true, "description": "Preview environments for every branch, on your own server, for your team and its coding agents", "license": "Apache-2.0", diff --git a/packages/core/package.json b/packages/core/package.json index 6d6f477..0c5d853 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@spawner/core", - "version": "2.1.0", + "version": "2.2.0", "private": true, "description": "Spawner manifest, compose policy and rendering (pure, shared by the API and the CLI)", "license": "Apache-2.0", diff --git a/packages/types/package.json b/packages/types/package.json index 9562ca6..8b5624b 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@spawner/types", - "version": "2.1.0", + "version": "2.2.0", "private": true, "license": "Apache-2.0", "main": "./dist/index.js", diff --git a/packages/utils/package.json b/packages/utils/package.json index 92cb97d..1b31bd0 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@spawner/utils", - "version": "2.1.0", + "version": "2.2.0", "private": true, "license": "Apache-2.0", "main": "./dist/index.js", diff --git a/server.json b/server.json index 8430540..9778823 100644 --- a/server.json +++ b/server.json @@ -10,13 +10,13 @@ "subfolder": "apps/cli" }, "websiteUrl": "https://spawner.run/docs/agents/", - "version": "2.1.0", + "version": "2.2.0", "packages": [ { "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": "spawner-cli", - "version": "2.1.0", + "version": "2.2.0", "transport": { "type": "stdio" },