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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ contact_links:
url: https://github.com/Flosk6/Spawner/security/advisories/new
about: Report it privately, never in an issue
- name: Documentation
url: https://github.com/Flosk6/Spawner#documentation
url: https://spawner.run/docs/
about: Installing, the manifest, the CLI, agents, security, operations
Binary file added .github/assets/dashboard-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/dashboard-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions .github/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
114 changes: 104 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ name: Release
# of its image, so that it installs what this workflow built.
# scripts/release.sh prepares the versions and the tag.
#
# Once npm has a stable version, its server.json goes to the official MCP
# Registry, which trusts this repository's OIDC token for the names
# io.github.Flosk6/* (the owner's case: the registry compares it as is). That
# job comes last and may fail without undoing or holding back anything else.
#
# The jobs that publish (image, npm, release) run in the "release"
# environment, whose reviewers approve twice per tag: first the image and npm
# together (both wait for the CLI build), then the GitHub release, which waits
# for the image. Nothing here reads a cache that another workflow could have
# written. Actions are pinned by commit (updated by Dependabot); the binfmt
# and BuildKit images by digest (updated by hand).
# and BuildKit images by digest, mcp-publisher by version and checksum
# (updated by hand).
#
# GitHub runs the workflow of the tagged commit: these guards hold for tags
# on commits that carry them, so creating v* tags is restricted to admins by
Expand All @@ -30,6 +36,7 @@ permissions:

env:
IMAGE: ghcr.io/flosk6/spawner
DESCRIPTION: "Spawner is an open source, self-hosted preview environment manager: a copy of your app for each git branch, on your own server, with its own URLs, database and logs, for teams and their coding agents."

jobs:
version:
Expand Down Expand Up @@ -94,10 +101,15 @@ jobs:
fs.writeFileSync("release/npm/package.json", JSON.stringify({
name: "spawner-cli",
version: process.env.SPAWNER_VERSION,
description: "Spawner CLI and MCP server: preview environments from a worktree, on your own server",
keywords: ["preview", "environments", "docker", "compose", "mcp", "coding agents"],
homepage: "https://github.com/Flosk6/Spawner",
description: "The CLI and MCP server of Spawner, an open source, self-hosted preview environment manager: a copy of your app for each git branch, on your own server, with its own URLs, database and logs.",
keywords: [
"preview-environments", "ephemeral-environments", "review-apps", "self-hosted", "docker-compose",
"mcp", "mcp-server", "coding-agents", "claude-code", "codex", "cursor", "devops", "cli",
],
homepage: "https://spawner.run",
repository: { type: "git", url: "git+https://github.com/Flosk6/Spawner.git", directory: "apps/cli" },
bugs: { url: "https://github.com/Flosk6/Spawner/issues" },
mcpName: require("./server.json").name,
license: "Apache-2.0",
bin: { spawner: "spawner.cjs" },
files: ["spawner.cjs", "README.md", "LICENSE", "NOTICE"],
Expand Down Expand Up @@ -143,14 +155,29 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# The labels and the annotations replace those of the Dockerfile and
# of the repository's settings; the annotations of the index are what
# GHCR shows for an image of several platforms.
- id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ${{ env.IMAGE }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
labels: |
org.opencontainers.image.title=Spawner
org.opencontainers.image.description=${{ env.DESCRIPTION }}
org.opencontainers.image.url=https://spawner.run
org.opencontainers.image.documentation=https://spawner.run/docs/
annotations: |
org.opencontainers.image.title=Spawner
org.opencontainers.image.description=${{ env.DESCRIPTION }}
org.opencontainers.image.url=https://spawner.run
org.opencontainers.image.documentation=https://spawner.run/docs/
- name: Build and push linux/amd64 and linux/arm64
id: build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
Expand All @@ -160,6 +187,7 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
build-args: SPAWNER_VERSION=${{ needs.version.outputs.version }}
- name: Attest the provenance of the images
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
Expand Down Expand Up @@ -239,17 +267,39 @@ jobs:
awk -v version="$VERSION" '
$0 ~ "^## " { if (found) exit; found = ($2 == version) ; next }
found { print }
' CHANGELOG.md > release/notes.md
[ -s release/notes.md ] || echo "Spawner $VERSION." > release/notes.md
cat >> release/notes.md <<EOF
' CHANGELOG.md > release/changes.md
[ -s release/changes.md ] || echo "Spawner $VERSION." > release/changes.md
download="https://github.com/Flosk6/Spawner/releases/download/v$VERSION"
{
echo "$DESCRIPTION [Documentation](https://spawner.run/docs/), [changelog](https://spawner.run/changelog/)."
echo
cat release/changes.md
cat <<EOF

## Install

On a new server:

\`\`\`bash
curl -fsSL $download/install.sh | sudo bash -s -- --version $VERSION
\`\`\`

## Upgrade

## Install or upgrade
From the dashboard: **Update to $VERSION** on the System page. From the server:

\`\`\`bash
curl -fsSL https://github.com/Flosk6/Spawner/releases/download/v$VERSION/install.sh | sudo bash -s -- --version $VERSION
curl -fsSL $download/install.sh | sudo bash -s -- --upgrade --version $VERSION
\`\`\`

Image: \`${IMAGE}:${VERSION}\` (\`${DIGEST}\`, linux/amd64, linux/arm64). CLI: \`npm install -g spawner-cli@$VERSION\`, or the \`spawner\` file below.
Both back the database up first, and go back to the previous version if the new one does not start.

## Files

- \`install.sh\`: the installer of this version.
- \`spawner\`: the CLI and MCP server in one file, for Node.js 20 or later. Or \`npm install -g spawner-cli@$VERSION\`.
- \`SHA256SUMS\`: their checksums.
- Image: \`${IMAGE}:${VERSION}\` (\`${DIGEST}\`, linux/amd64, linux/arm64).

## Verify

Expand All @@ -259,7 +309,10 @@ jobs:
gh attestation verify install.sh -R Flosk6/Spawner
gh attestation verify oci://${IMAGE}:$VERSION -R Flosk6/Spawner
\`\`\`

Or compare the files with their checksums: \`curl -fsSL -O $download/install.sh -O $download/SHA256SUMS && sha256sum -c --ignore-missing SHA256SUMS\`
EOF
} > release/notes.md
- name: Attest the provenance of install.sh and the CLI bundle
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
Expand All @@ -271,3 +324,44 @@ jobs:
gh release create "v$VERSION" release/install.sh release/spawner release/SHA256SUMS \
--title "Spawner $VERSION" --notes-file release/notes.md \
${{ needs.version.outputs.prerelease == 'true' && '--prerelease' || '--latest' }}

mcp-registry:
name: MCP server on the MCP Registry
needs: [version, npm]
if: needs.version.outputs.prerelease == 'false'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
VERSION: ${{ needs.version.outputs.version }}
MCP_PUBLISHER_VERSION: 1.8.1
MCP_PUBLISHER_SHA256: a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install mcp-publisher, checked against its checksum
run: |
curl -fsSL -o mcp-publisher.tar.gz "https://github.com/modelcontextprotocol/registry/releases/download/v$MCP_PUBLISHER_VERSION/mcp-publisher_linux_amd64.tar.gz"
echo "$MCP_PUBLISHER_SHA256 mcp-publisher.tar.gz" | sha256sum -c -
tar -xzf mcp-publisher.tar.gz mcp-publisher
- name: Wait until npm serves spawner-cli@${{ needs.version.outputs.version }} with its mcpName
run: |
name="$(jq -r .name server.json)"
for _ in $(seq 1 30); do
[ "$(curl -fsS "https://registry.npmjs.org/spawner-cli/$VERSION" 2>/dev/null | jq -r .mcpName)" = "$name" ] && exit 0
sleep 10
done
echo "::error::spawner-cli@$VERSION on npm has no mcpName $name: the MCP Registry would refuse it"
exit 1
- name: Publish server.json at version ${{ needs.version.outputs.version }}
run: |
jq --arg version "$VERSION" '.version = $version | .packages[].version = $version' server.json > release-server.json
name="$(jq -r .name release-server.json)"
if curl -fsS -o /dev/null "https://registry.modelcontextprotocol.io/v0.1/servers/$(jq -rn --arg name "$name" '$name | @uri')/versions/$VERSION"; then
echo "::notice::$name $VERSION is already on the MCP Registry"
exit 0
fi
./mcp-publisher login github-oidc
./mcp-publisher publish release-server.json
3 changes: 1 addition & 2 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"Vue.volar",
"firsttris.vscode-jest-runner",
"ms-vscode.vscode-typescript-next"
"vitest.explorer"
]
}
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
- **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`.

## 2.1.0

Expand All @@ -37,7 +43,7 @@ A rewrite of Spawner: preview environments for every branch, for teams and the c
- **The `spawner` CLI and its MCP server**, for people and coding agents: `up` from a worktree, `exec` in a service, logs with an error filter, URLs with a preview token, share links; `--json` everywhere and stable exit codes. `npm install -g spawner-cli`.
- **Access without passwords**: invitation links and passkeys, optional GitHub login, roles, personal tokens with scopes, a device flow for the CLI. Previews are protected: teammates pass with their session, agents with a short token, guests with a share link, and the applications never see what opens the other previews. Everything goes to an audit trail.
- **Supervision**: CPU and memory of every container over 30 days, disk per environment, a timeline of crashes, out-of-memory kills and jobs, crash loop alerts, logs kept 7 days after a deletion, terminals with limits and recordings.
- **Lifecycle and density**: environments sleep after 2 hours without activity and wake up on the next visit, expire after 72 hours, and count against a quota per person; Spawner refuses an environment the server has no room for, removes replaced images and the code a build no longer needs, and cleans up what deleted environments leave, nothing else.
- **Lifecycle and density**: environments sleep after 2 hours without activity and wake up on the next visit, expire after 72 hours, and count against a quota per person; Spawner refuses an environment beyond the server's capacity, removes replaced images and the code a build no longer needs, and cleans up what deleted environments leave, nothing else.
- **One-command install**: `install.sh` sets up Docker, a wildcard certificate through your DNS provider (or one per host), and the stack from the images on GHCR; it upgrades (with a database backup) and uninstalls too.
- **Updates from the dashboard**: Spawner sees new versions and updates itself in one click, backing its database up first and going back to the previous version if the new one does not start.
- **Examples and documentation**: Node.js with PostgreSQL, and Laravel, Next.js and MySQL; guides for installing, agents, security, operations and density.
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ RUN pnpm --filter @spawner/api --prod deploy /out \
FROM base AS runtime
ARG SPAWNER_VERSION=
LABEL org.opencontainers.image.title="Spawner" \
org.opencontainers.image.description="Preview environments for every branch, on your own server" \
org.opencontainers.image.description="Spawner is an open source, self-hosted preview environment manager: a copy of your app for each git branch, on your own server, with its own URLs, database and logs, for teams and their coding agents." \
org.opencontainers.image.url="https://spawner.run" \
org.opencontainers.image.documentation="https://spawner.run/docs/" \
org.opencontainers.image.source="https://github.com/Flosk6/Spawner" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.version="${SPAWNER_VERSION}"
Expand Down
Loading
Loading