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
13 changes: 8 additions & 5 deletions .github/workflows/cd-deploy-whygraph.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
name: Deploy WhyGraph Image

# Builds and pushes ghcr.io/mtrdesign/whygraph — the self-contained
# scanning-service image the `whygraph` host shim (scripts/install.sh)
# runs. Publishing is tied to releases: cutting a GitHub release (tagged
# vX.Y.Z) builds the image and tags it to match that version, so the image
# tag follows the release version 1:1.
# scanning-service image. It is the single install channel:
# `docker run --rm ghcr.io/mtrdesign/whygraph install | sh` emits the host
# shims (via the in-image `whygraph install` command). Publishing is tied to
# releases: cutting a GitHub release (tagged vX.Y.Z) builds the image, tags it
# to match that version, and bakes WHYGRAPH_VERSION so the shims pin that
# release. The image tag follows the release version 1:1.

on:
release:
Expand Down Expand Up @@ -60,6 +62,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
CODEGRAPH_VERSION=latest
CODEGRAPH_VERSION=1.4.1
WHYGRAPH_VERSION=${{ steps.meta.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ There is no Claude Code marketplace install; `whygraph init --agent claude` is t

## Docker delivery (the default install)

WhyGraph ships as a self-contained image so a developer needs **only Docker** on the host — no Python / Node / gh / codegraph install. The whole UX is three steps:
WhyGraph ships as a self-contained image so a developer needs **only Docker** on the host — no Python / Node / gh / codegraph install. The **image is the single install channel** — there is no GitHub-hosted install script. The whole UX is three steps:

```bash
curl -fsSL https://raw.githubusercontent.com/mtrdesign/whygraph/main/scripts/install.sh | sh
docker run --rm ghcr.io/mtrdesign/whygraph install | sh # tag selects version; :latest is default
whygraph init # in a repo
whygraph scan
```

`scripts/install.sh` drops a ~10-line `whygraph` (and `whygraph-mcp`) **shim** on PATH that wraps `docker run --rm -v "$PWD:/workspace" -w /workspace <image> whygraph "$@"` and pulls `ghcr.io/mtrdesign/whygraph`. The container is **ephemeral per command** — there is no compose, no `docker exec`, no long-running container. The image is built from `docker/whygraph/Dockerfile` (base `python:3.12-slim` + git + gh + Node 22 + pinned CodeGraph CLI + WhyGraph) and published by `.github/workflows/publish-whygraph-image.yml`.
The in-image **`whygraph install` command** (`cli/commands/install.py`) prints a POSIX `sh` installer to stdout; piping it to `sh` writes the `whygraph` (and `whygraph-mcp`) **shims** onto PATH, each wrapping `docker run --rm -v "$PWD:/workspace" -w /workspace <image> whygraph "$@"`. `docker run … install` is reached via a tiny **image-only launcher** (`/usr/local/bin/install` → `exec whygraph install`); there is deliberately **no `ENTRYPOINT`** so `docker run <image> codegraph …` and `<image> whygraph-mcp` still resolve. The shims bake `ghcr.io/mtrdesign/whygraph:<version>` from the image's `WHYGRAPH_VERSION` (baked at build), so an install **pins the concrete release** even via `:latest`; each shim still honours a `WHYGRAPH_IMAGE` override at run time. The container is **ephemeral per command** — no compose, no `docker exec`, no long-running container. The image is built from `docker/whygraph/Dockerfile` (base `python:3.12-slim` + git + gh + Node 22 + pinned CodeGraph CLI + WhyGraph) and published by `.github/workflows/cd-deploy-whygraph.yml` (which passes `WHYGRAPH_VERSION` + a pinned `CODEGRAPH_VERSION` as build-args).

Invariants that keep the shim correct — preserve them:

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ whygraph init --agent claude # wire the MCP server into your editor
whygraph-mcp # sanity-check the server (Ctrl-C to exit)
```

The only-Docker install needs nothing but Docker on the host:
The only-Docker install needs nothing but Docker on the host — one command pulls the image and
installs the shims from inside it (pin a version with the image tag):

```bash
curl -fsSL https://raw.githubusercontent.com/mtrdesign/whygraph/main/scripts/install.sh | sh
docker run --rm ghcr.io/mtrdesign/whygraph install | sh
```

See the [Getting Started guide](https://mtrdesign.github.io/whygraph/getting-started/) for every install path and the [Quickstart](https://mtrdesign.github.io/whygraph/getting-started/quickstart/) for the walkthrough.
Expand Down
18 changes: 18 additions & 0 deletions docker/whygraph/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,31 @@ RUN set -eux; \
apt-get purge -y --auto-remove gnupg; \
rm -rf /var/lib/apt/lists/*

# WhyGraph's own version, baked so `whygraph install` can pin the generated
# shims to a concrete release even when the image is pulled via `:latest`. The
# publish workflow passes the release tag; `latest` is the local-build default.
# Declared here (not with the top ARGs) so bumping it never busts the
# apt/node/codegraph layers above.
ARG WHYGRAPH_VERSION=latest
ENV WHYGRAPH_VERSION=${WHYGRAPH_VERSION}

# Install WhyGraph itself so the `whygraph` and `whygraph-mcp` console
# scripts land on PATH. Copy the build inputs hatchling needs and install.
WORKDIR /opt/whygraph
COPY pyproject.toml ./
COPY src ./src
RUN pip install --no-cache-dir .

# Image-only shortcut so `docker run IMAGE install | sh` works without an
# ENTRYPOINT — an entrypoint of `whygraph` would break `docker run IMAGE
# codegraph …` (services/codegraph/bootstrap.py) and the whygraph-mcp shim.
# Deliberately NOT a pyproject [project.scripts] entry, so a native
# `uv tool install` never drops a bare `install` on a user's PATH (it would
# shadow coreutils `install`). Added last so no earlier build step depends on
# coreutils `install`.
RUN printf '#!/bin/sh\nexec whygraph install "$@"\n' > /usr/local/bin/install \
&& chmod +x /usr/local/bin/install

# The host shim bind-mounts the current repo here and sets the command
# (e.g. `whygraph scan`). A bare `docker run IMAGE` prints CLI help.
WORKDIR /workspace
Expand Down
15 changes: 9 additions & 6 deletions docs/deploy/docker.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
# Run with Docker

Don't want Python, Node, `gh`, and CodeGraph on your machine? WhyGraph ships as a self-contained
image. Your host needs **only Docker**. Install a tiny shim, then it's the same `init` and `scan` as a
native install.
image. Your host needs **only Docker**. One command installs the shims from inside the image, then
it's the same `init` and `scan` as a native install.

```bash
curl -fsSL https://raw.githubusercontent.com/mtrdesign/whygraph/main/scripts/install.sh | sh
docker run --rm ghcr.io/mtrdesign/whygraph install | sh

cd your-repo
whygraph init # bootstrap the WhyGraph DB + write config
whygraph scan # crawl history + refresh CodeGraph + LLM descriptions
```

Pick a version with the image tag - `docker run --rm ghcr.io/mtrdesign/whygraph:1.2.3 install | sh`
pins that release; `:latest` (the default) installs the newest.

## How the shim works

`install.sh` drops `whygraph` and `whygraph-mcp` shims on your `PATH`. Each one runs the published
image against the current directory:
`docker run … install` prints a short installer to stdout; piping it to `sh` drops `whygraph` and
`whygraph-mcp` shims on your `PATH`. Each one runs the published image against the current directory:

```bash
docker run --rm -v "$PWD:/workspace" -w /workspace ghcr.io/mtrdesign/whygraph whygraph "$@"
Expand Down Expand Up @@ -43,7 +46,7 @@ The shim passes your environment through. A GitHub token goes in `[scan].token`

## Wire your editor, still only Docker

The MCP server is containerized too. `install.sh` drops a `whygraph-mcp` shim alongside `whygraph`, so
The MCP server is containerized too. The installer drops a `whygraph-mcp` shim alongside `whygraph`, so
there's nothing extra to install. Wire your editor from inside the repo:

```bash
Expand Down
4 changes: 2 additions & 2 deletions docs/deploy/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ docker run --rm -i \
```

The `-i` flag keeps stdin open for the MCP stdio transport. Your app spawns this command and talks
JSON-RPC to it, exactly as an editor would. The [`install.sh` shim](docker.md) wraps the same call as
a bare `whygraph-mcp` on `PATH`.
JSON-RPC to it, exactly as an editor would. The [`whygraph-mcp` shim](docker.md) — dropped by
`docker run … install | sh` — wraps the same call as a bare `whygraph-mcp` on `PATH`.

## Credentials

Expand Down
10 changes: 6 additions & 4 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ Pick the path that fits where you are.

=== "Docker (recommended)"

The host needs **only Docker** - no Python, Node, `gh`, or CodeGraph. A tiny shim runs everything
inside one published image.
The host needs **only Docker** - no Python, Node, `gh`, or CodeGraph. One command pulls the
published image and installs the shims from inside it:

```bash
curl -fsSL https://raw.githubusercontent.com/mtrdesign/whygraph/main/scripts/install.sh | sh
docker run --rm ghcr.io/mtrdesign/whygraph install | sh
```

This drops `whygraph` and `whygraph-mcp` shims on your `PATH`. Each wraps a
Pin a specific version with the image tag - `docker run --rm ghcr.io/mtrdesign/whygraph:1.2.3
install | sh`; `:latest` (the default) installs the newest release. This drops `whygraph` and
`whygraph-mcp` shims on your `PATH`. Each wraps a
`docker run --rm -v "$PWD:/workspace" … ghcr.io/mtrdesign/whygraph` against the current repo. The
container is ephemeral per command. See [Run with Docker](../deploy/docker.md) for the full story.

Expand Down
80 changes: 0 additions & 80 deletions scripts/install.sh

This file was deleted.

2 changes: 2 additions & 0 deletions src/whygraph/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .commands.analyze import analyze_cmd
from .commands.hooks import hooks_cmd
from .commands.init import init_cmd
from .commands.install import install_cmd
from .commands.scan import scan_cmd
from .commands.version import version_cmd

Expand All @@ -32,3 +33,4 @@ def main() -> None:
main.add_command(scan_cmd)
main.add_command(analyze_cmd)
main.add_command(hooks_cmd)
main.add_command(install_cmd)
129 changes: 129 additions & 0 deletions src/whygraph/cli/commands/install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""The ``whygraph install`` subcommand — emit the host shim installer.

WhyGraph ships as a single Docker image. This command prints a POSIX ``sh``
installer to **stdout** so the whole install is one Docker-native line::

docker run --rm ghcr.io/mtrdesign/whygraph install | sh

The image auto-pulls on first use, this command emits the installer, and the
host ``sh`` writes two shims — ``whygraph`` and ``whygraph-mcp`` — onto
``PATH``. Each shim runs the image ephemerally against the current directory
(``docker run --rm -v "$PWD:/workspace" …``), so every command is a fresh
process reading that repo's own ``whygraph.toml`` / ``.whygraph`` / ``.codegraph``.

The image reference baked into the shims is ``ghcr.io/mtrdesign/whygraph`` at
the version baked into the image at build time (``WHYGRAPH_VERSION`` env, set by
the publish workflow), so an install freezes to a **concrete release** even when
pulled via ``:latest``. Each shim still honours a ``WHYGRAPH_IMAGE`` override at
run time for mirrors / local builds.

Notes
-----
Only the installer script is written to stdout (via :func:`click.echo`); all
logging goes to stderr. That keeps the ``… | sh`` pipe clean — the container's
stderr surfaces on the terminal but never corrupts the piped script.
"""

from __future__ import annotations

import os

import click

IMAGE_REPO = "ghcr.io/mtrdesign/whygraph"
"""Canonical published image; the baked default for the generated shims."""

_SHIM_TEMPLATE = """\
#!/usr/bin/env sh
# WhyGraph shim — runs '__NAME__' inside the WhyGraph container against the
# current directory. Generated by `whygraph install`; safe to re-generate.
set -eu
IMAGE="${WHYGRAPH_IMAGE:-__IMAGE__}"

# Allocate a TTY only when attached to one, so progress bars render
# interactively but pipes / CI / MCP-over-stdio still work.
tty=""
[ -t 0 ] && [ -t 1 ] && tty="-t"

# --user + HOME=/tmp so files written into the repo (.whygraph/, .codegraph/)
# are owned by the host user and git sees matching ownership (no "dubious
# ownership"). Tokens / LLM keys pass through from the host env when set.
exec docker run --rm -i $tty \\
--user "$(id -u):$(id -g)" -e HOME=/tmp \\
-v "$PWD:/workspace" -w /workspace \\
-e GH_TOKEN -e GITHUB_TOKEN \\
-e ANTHROPIC_API_KEY -e OPENAI_API_KEY -e DEEPSEEK_API_KEY \\
"$IMAGE" __NAME__ "$@"
"""

_INSTALLER_TEMPLATE = """\
#!/usr/bin/env sh
# WhyGraph installer — emitted by `whygraph install` from inside the image.
# Writes the `whygraph` and `whygraph-mcp` shims onto your PATH; each runs this
# image ephemerally against the current directory. Re-run any time to refresh.
set -eu

BIN_DIR="${WHYGRAPH_BIN_DIR:-$HOME/.local/bin}"
mkdir -p "$BIN_DIR"

cat > "$BIN_DIR/whygraph" <<'WHYGRAPH_SHIM'
__WHYGRAPH_SHIM__
WHYGRAPH_SHIM
chmod +x "$BIN_DIR/whygraph"
echo "installed $BIN_DIR/whygraph"

cat > "$BIN_DIR/whygraph-mcp" <<'WHYGRAPH_MCP_SHIM'
__WHYGRAPH_MCP_SHIM__
WHYGRAPH_MCP_SHIM
chmod +x "$BIN_DIR/whygraph-mcp"
echo "installed $BIN_DIR/whygraph-mcp"

case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*) echo "note: add $BIN_DIR to your PATH, e.g. 'export PATH=\\"$BIN_DIR:\\$PATH\\"'" ;;
esac

echo "done. Try: cd <your-repo> && whygraph init && whygraph scan"
"""


def _shim(name: str, image: str) -> str:
"""Render one shim's body for ``name``, baking ``image`` as its default."""
return _SHIM_TEMPLATE.replace("__NAME__", name).replace("__IMAGE__", image)


def render_installer(image: str) -> str:
"""Render the full installer script that writes both shims for ``image``.

Parameters
----------
image : str
The image reference baked as each shim's ``WHYGRAPH_IMAGE`` default,
e.g. ``ghcr.io/mtrdesign/whygraph:1.2.3``.

Returns
-------
str
A self-contained POSIX ``sh`` script, newline-terminated.
"""
return _INSTALLER_TEMPLATE.replace(
"__WHYGRAPH_MCP_SHIM__", _shim("whygraph-mcp", image).rstrip("\n")
).replace("__WHYGRAPH_SHIM__", _shim("whygraph", image).rstrip("\n"))


@click.command(name="install")
def install_cmd() -> None:
"""Emit the host shim installer (`docker run … install | sh`).

Prints a POSIX ``sh`` script that installs the ``whygraph`` and
``whygraph-mcp`` shims onto ``PATH``. The shims are pinned to
``ghcr.io/mtrdesign/whygraph`` at this image's baked ``WHYGRAPH_VERSION``
(``latest`` for un-versioned local builds), overridable per command via the
``WHYGRAPH_IMAGE`` environment variable.
"""
version = os.environ.get("WHYGRAPH_VERSION", "latest")
image = f"{IMAGE_REPO}:{version}"
click.echo(render_installer(image), nl=False)


__all__ = ["install_cmd", "render_installer", "IMAGE_REPO"]
Loading
Loading