Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
# 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`.
- Accounts: passkeys must verify their user (a PIN, a fingerprint); names are unique, whatever their case; renames and linked GitHub accounts are audited, and every audit event records the address of its request; the CLI approval page says when and from where the login started. Removing someone from the GitHub organization does not deactivate them in Spawner: do it on the Team page.
- 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.
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@spawner/web",
"version": "2.1.0",
"version": "2.2.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
16 changes: 13 additions & 3 deletions scripts/release.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Loading