easy-install.sh — Documentation ================================ ======================= Connecting Your Project ======================= ## Quick Start ### Connect a repo 1. **Sign in** at `easy-install.sh/app` 2. **Install the GitHub App** — on the repos page, click **Install GitHub App** and select which repos to share 3. **Connect** — click **Connect** next to any repo in the list Your repo is now live at `easy-install.sh/{owner}/{repo}`. --- ## Organization Repos ### Adding repos from an org The GitHub App is installed per-account. To add org repos, click **Manage repo access on GitHub** on the repos page, switch to the org, and install the app there. --- ## Managing Access ### Change shared repos Click **Manage repo access on GitHub** to add or remove repos from your installation. ### Disconnect a repo Click the trash icon next to any connected repo on the repos page. ### Uninstall the app GitHub settings > Applications > easy-install.sh > Uninstall. --- ## After Connecting ### Vanity URLs | URL pattern | What it does | | -------------------------------------------- | ----------------------------------- | | `easy-install.sh/{owner}/{repo}/install.sh` | Proxies a script from your repo | | `docker pull easy-install.sh/{owner}/{repo}` | Pulls a Docker image from GHCR | | `easy-install.sh/{owner}/{repo}/{path}` | Proxies any raw file from your repo | See [Docker](/docs/docker), [Scripts](/docs/scripts), and [Content](/docs/content) for details. --- ============= Docker Images ============= ## Quick Start ### Pull your first image Replace `namespace` and `project` with your GitHub username/org and repo name. ``` docker pull easy-install.sh/acme/api ``` --- ## Deep Dive ### How it works When you run `docker pull easy-install.sh/acme/api`, here's what happens under the hood: 1. **Registry handshake** Docker sends `GET /v2/` to verify that easy-install.sh speaks the OCI Distribution spec. The proxy responds with `200 OK` and the required headers. ``` GET https://easy-install.sh/v2/ ``` 2. **Manifest request** Docker requests the image manifest. The proxy fetches it from GHCR using an anonymous token and streams the response back. ``` GET https://easy-install.sh/v2/acme/api/manifests/latest ``` 3. **Analytics recorded** The proxy fires off an analytics event (fire-and-forget) with the pull metadata. This never blocks the Docker pull. The pull continues regardless of whether analytics succeeds. 4. **Blob streaming** Docker fetches the image layers (blobs). Each blob request is proxied through to GHCR and streamed directly to the Docker client. The proxy never buffers entire layers in memory. ``` GET https://easy-install.sh/v2/acme/api/blobs/sha256:abc123... ``` 5. **Image ready** Once all layers are pulled, the image is available locally just like any other Docker image. Run it, tag it, push it elsewhere. It's a standard OCI image. --- ## Routing ### Namespace mapping The URL path maps directly to a GHCR image path. No configuration required. The namespace and project in your URL correspond exactly to your GitHub identity and repository. | easy-install.sh URL | GHCR Path | | ------------------- | ---------------------- | | acme/api | ghcr.io/acme/api | | jane/cli-tool | ghcr.io/jane/cli-tool | | my-org/backend | ghcr.io/my-org/backend | --- ## Tags ### Tag resolution Tags work exactly like any Docker registry. If omitted, Docker defaults to `:latest`. **Latest (default)** ``` docker pull easy-install.sh/acme/api ``` Equivalent to `easy-install.sh/acme/api:latest` **Explicit tag** ``` docker pull easy-install.sh/acme/api:v2.1.0 ``` **Digest reference** ``` docker pull easy-install.sh/acme/api@sha256:a1b2c3d4... ``` Pinning by digest guarantees you get the exact same image every time. --- ## Security ### Authentication GHCR requires a token even for public images. The proxy handles this automatically using the standard Docker token exchange flow: 1. **Initial request (unauthenticated)** — The proxy calls GHCR without credentials. GHCR responds with `401 Unauthorized` and a `Www-Authenticate` header pointing to its token endpoint. 2. **Token exchange** — The proxy requests an anonymous token from GHCR's token service, scoped to the specific repository being pulled. 3. **Authenticated retry** — The proxy retries the original request with the token in the `Authorization: Bearer` header. GHCR serves the content. > [!NOTE] > You never need to `docker login` to easy-install.sh. The proxy handles all token exchange behind the scenes for public images. --- ## Analytics ### What gets tracked Each Docker pull records the following anonymous metadata. No personal information is collected. | Field | Description | | ------------ | ------------------------------ | | Namespace | GitHub user or org | | Project | Repository name | | Tag / Digest | Which version was pulled | | Timestamp | When the pull occurred (UTC) | | Country | Derived from IP via geo lookup | | User-Agent | Docker client version string | > [!NOTE] > IP addresses are used only for geo lookup and are never stored. Analytics are fully anonymous. --- ## Examples ### Common usage **Pull and run** ``` docker pull easy-install.sh/acme/api:latest docker run -p 8080:8080 easy-install.sh/acme/api:latest ``` **Use in a Dockerfile** ```dockerfile filename="Dockerfile" FROM easy-install.sh/acme/api:v2.1.0 COPY ./config /app/config CMD ["/app/server"] ``` **Docker Compose** ```yaml filename="docker-compose.yml" services: api: image: easy-install.sh/acme/api:v2 ports: - '8080:8080' ``` **Pin to a specific digest** ``` docker pull easy-install.sh/acme/api@sha256:a1b2c3d4e5f6... ``` --- ## Limitations ### Current limitations > [!WARNING] GHCR only > Docker forwarding currently only supports GitHub Container Registry (ghcr.io). Docker Hub, ECR, and other registries are not supported. --- ============= Shell Scripts ============= ## Quick Start ### Run your first script Replace `namespace` and `project` with your GitHub username/org and repo name. ``` curl -sSL easy-install.sh/acme/cli/install.sh | sh ``` --- ## Deep Dive ### How it works When a user runs `curl -sSL easy-install.sh/acme/cli/install.sh | sh`, here is what happens: 1. **Namespace resolution** The proxy parses the URL into namespace (`acme`), project (`cli`), and path (`install.sh`). The namespace is validated against the allowlist. 2. **Branch lookup** The proxy resolves the repository's default branch using the GitHub API. The result is cached in the database so subsequent requests skip this step. 3. **Upstream fetch** The proxy fetches the file from GitHub's raw content endpoint and streams the response directly to the client. The file is never buffered in memory. ``` GET https://raw.githubusercontent.com/acme/cli/main/install.sh ``` 4. **Analytics recorded** An analytics event fires in the background with the download metadata. This is fire-and-forget and never blocks the response. The download continues regardless of whether analytics succeeds. 5. **Script delivered** The script content is streamed to the client with the correct `Content-Type` header. If piped to a shell, it executes immediately. --- ## Routing ### Path resolution The URL path after the project name maps directly to a file path in the repository. Files are always fetched from the repository's default branch. | easy-install.sh URL | Upstream Path | | ----------------------------------- | ------------------------------------------------------------------ | | acme/cli/install.sh | raw.githubusercontent.com/acme/cli/main/install.sh | | jane/dotfiles/setup.sh | raw.githubusercontent.com/jane/dotfiles/main/setup.sh | | my-org/infra/scripts/bootstrap.bash | raw.githubusercontent.com/my-org/infra/main/scripts/bootstrap.bash | > [!NOTE] > Nested paths work too. If your script lives at `scripts/install.sh` in your repo, use `easy-install.sh/you/repo/scripts/install.sh`. --- ## Detection ### Script detection The proxy identifies script downloads by file extension. Files ending in `.sh`, `.bash`, or `.zsh` are tracked as script hits in your analytics dashboard. All other file extensions are tracked as content hits. | Category | Extensions | | -------------------- | -------------------- | | Tracked as "script" | .sh, .bash, .zsh | | Tracked as "content" | All other extensions | --- ## Headers ### Response headers The proxy sets the following headers on every response: | Header | Value | | -------------- | ----------------------------------------------------------------------------- | | Content-Type | MIME type guessed from file extension (e.g. text/x-shellscript for .sh files) | | Cache-Control | public, max-age=300 (5-minute cache) | | X-Proxied-From | raw.githubusercontent.com (informational) | --- ## Analytics ### What gets tracked Each script download records the following anonymous metadata. No personal information is collected. | Field | Description | | -------------- | ---------------------------------------------------- | | Namespace | GitHub user or org | | Project | Repository name | | File Path | Which file was downloaded | | Hit Type | "script" for .sh/.bash/.zsh files | | Client Tool | Detected from User-Agent (curl, wget, browser, etc.) | | Client Version | Version string extracted from User-Agent | | Timestamp | When the download occurred (UTC) | | Country | Derived from IP via geo lookup | > [!NOTE] > IP addresses are hashed with a one-way SHA-256 function and never stored in plaintext. Analytics are fully anonymous. --- ## Examples ### Common usage **Pipe to shell** ``` curl -sSL easy-install.sh/acme/cli/install.sh | sh ``` **Pipe to bash explicitly** ``` curl -sSL easy-install.sh/acme/cli/install.sh | bash ``` **Download first, then run** ``` curl -sSL easy-install.sh/acme/cli/install.sh -o install.sh chmod +x install.sh ./install.sh ``` **Pass arguments to the script** ``` curl -sSL easy-install.sh/acme/cli/install.sh | sh -s -- --prefix=/usr/local ``` **Use with wget** ``` wget -qO- easy-install.sh/acme/cli/install.sh | sh ``` **Nested path in repo** ``` curl -sSL easy-install.sh/acme/infra/scripts/bootstrap.sh | sh ``` **Include in a README** ```md filename="README.md" ## Install \`\`\`sh curl -sSL easy-install.sh/acme/cli/install.sh | sh \`\`\` ``` --- ## Limitations ### Current limitations > [!WARNING] Public repos only > easy-install.sh can only proxy files from public GitHub repositories. Private repos require authentication that the proxy cannot forward. > [!WARNING] Default branch only > Scripts are always fetched from the repository's default branch. There is no way to specify a tag, branch, or commit hash in the URL yet. > [!NOTE] No checksum verification > The proxy streams the file as-is from GitHub. It does not verify checksums or signatures. If integrity matters, consider adding verification steps in your script. > [!NOTE] 5-minute cache > Responses are cached for 5 minutes (`Cache-Control: public, max-age=300`). After pushing changes to your repo, it may take up to 5 minutes for the proxy to serve the updated file. --- ========= Git Clone ========= ## Quick Start ### Clone your first repo Replace `namespace` and `project` with your GitHub username/org and repo name. ``` git clone easy-install.sh/acme/cli ``` --- ## Deep Dive ### How it works When a user runs `git clone easy-install.sh/acme/cli`, here is what happens: 1. **Ref discovery** Git sends a GET request to discover available refs. easy-install.sh detects the `git/` User-Agent and responds with a 307 redirect to GitHub. ``` GET https://easy-install.sh/acme/cli/info/refs?service=git-upload-pack → 307 → https://github.com/acme/cli.git/info/refs?service=git-upload-pack ``` 2. **Object fetch** Git follows the redirect and negotiates objects directly with GitHub. All data transfer happens between the git client and GitHub — easy-install.sh is not in the data path. ``` POST https://github.com/acme/cli.git/git-upload-pack ``` 3. **Analytics recorded** An analytics event fires in the background when the initial request arrives. This is fire-and-forget and never blocks the redirect. The clone continues regardless of whether analytics succeeds. 4. **Repository cloned** The repo is cloned to a local directory just like any other git clone. The remote origin will point to GitHub, so subsequent pushes and pulls go directly to GitHub. --- ## Routing ### URL patterns Git clone requests are detected by the `git/` User-Agent header. Both URL styles work: | easy-install.sh URL | Redirects to | | ------------------- | ---------------------------- | | acme/cli | github.com/acme/cli.git | | acme/cli.git | github.com/acme/cli.git | | jane/dotfiles | github.com/jane/dotfiles.git | | my-org/infra.git | github.com/my-org/infra.git | The `.git` suffix is optional — easy-install.sh strips it before looking up the project and always adds it back in the redirect to GitHub. --- ## Detection ### Git client detection The proxy identifies git clone requests by the User-Agent header. Any request with a User-Agent starting with `git/` is treated as a clone request and redirected. | User-Agent | Detected As | | ---------------------- | ----------- | | git/2.43.0 | git clone | | git/2.39.3 (Apple Git) | git clone | | curl/7.81.0 | Not git | | Mozilla/5.0 | Not git | > [!NOTE] > Non-git clients hitting the same URL will get the normal project info page or file proxy, not a redirect. The behavior is determined entirely by User-Agent. --- ## Analytics ### What gets tracked Each git clone records the following anonymous metadata. No personal information is collected. | Field | Description | | -------------- | -------------------------------- | | Namespace | GitHub user or org | | Project | Repository name | | Hit Type | "clone" | | Client Tool | "git" | | Client Version | Git version string (e.g. 2.43.0) | | Timestamp | When the clone occurred (UTC) | | Country | Derived from IP via geo lookup | > [!NOTE] > IP addresses are hashed with a one-way SHA-256 function and never stored in plaintext. Analytics are fully anonymous. --- ## Examples ### Common usage **Basic clone** ``` git clone easy-install.sh/acme/cli ``` **Clone with .git suffix** ``` git clone easy-install.sh/acme/cli.git ``` **Clone to a specific directory** ``` git clone easy-install.sh/acme/cli my-project ``` **Shallow clone (depth 1)** ``` git clone --depth 1 easy-install.sh/acme/cli ``` **Include in a README** ```md filename="README.md" ## Install \`\`\`sh git clone easy-install.sh/acme/cli cd cli && make install \`\`\` ``` --- ## Limitations ### Current limitations > [!WARNING] Public repos only > easy-install.sh can only redirect to public GitHub repositories. Private repos require authentication that the redirect cannot forward. > [!NOTE] Redirect-based > Git clone uses a 307 redirect to GitHub rather than proxying the data. This means the remote origin in the cloned repo will point to GitHub, not easy-install.sh. > [!NOTE] HTTPS only > Git clone over SSH (`git@github.com:...`) is not supported. Only HTTPS clone URLs work through easy-install.sh. --- ============= Content Pipes ============= ## Quick Start ### Fetch your first file Replace `namespace`, `project`, and `path` with your GitHub username/org, repo name, and file path. ``` curl -sSL easy-install.sh/acme/app/config.yaml > config.yaml ``` --- ## Deep Dive ### How it works When a user fetches `easy-install.sh/acme/app/config.yaml`, here is what happens: 1. **Namespace resolution** The proxy parses the URL into namespace (`acme`), project (`app`), and path (`config.yaml`). The namespace is validated against the allowlist. 2. **Branch lookup** The proxy resolves the default branch via the GitHub API and caches the result. The file is always fetched from the default branch. 3. **Upstream fetch** The proxy fetches the file from GitHub's raw content endpoint and streams it directly to the client. No buffering in memory. ``` GET https://raw.githubusercontent.com/acme/app/main/config.yaml ``` 4. **MIME type detection** The proxy guesses the correct `Content-Type` from the file extension (e.g. `application/json` for `.json`, `text/yaml` for `.yaml`). If the extension is unrecognized, it falls back to the upstream header or `application/octet-stream`. 5. **Analytics recorded** An analytics event fires in the background (fire-and-forget). The download is never blocked by analytics. Content files are tracked separately from script files in your dashboard. --- ## MIME Types ### Content-Type handling The proxy detects the correct MIME type from the file extension so downstream tools can parse the response correctly. | Extension | Content-Type | | ------------ | ----------------------------------- | | .json | application/json | | .yaml / .yml | text/yaml | | .toml | text/toml | | .xml | application/xml | | .txt | text/plain | | .md | text/markdown | | .csv | text/csv | | .env | text/plain | | unknown | application/octet-stream (fallback) | --- ## Detection ### Scripts vs content Content pipes and script forwarding use the same URL pattern and the same proxy. The only difference is how the hit is categorized in analytics: | Category | Extensions | | ------------ | ----------------------------------- | | Script hits | Files ending in .sh, .bash, or .zsh | | Content hits | All other file extensions | > [!NOTE] > Both types appear separately in your analytics dashboard, so you can see how many users are downloading your install script vs your config files. --- ## Headers ### Response headers The proxy sets the following headers on every response: | Header | Value | | -------------- | ----------------------------------------- | | Content-Type | MIME type guessed from file extension | | Cache-Control | public, max-age=300 (5-minute cache) | | X-Proxied-From | raw.githubusercontent.com (informational) | --- ## Analytics ### What gets tracked Each content download records the following anonymous metadata. No personal information is collected. | Field | Description | | -------------- | ---------------------------------------------------- | | Namespace | GitHub user or org | | Project | Repository name | | File Path | Which file was downloaded | | Hit Type | "content" for non-script files | | Client Tool | Detected from User-Agent (curl, wget, browser, etc.) | | Client Version | Version string extracted from User-Agent | | Timestamp | When the download occurred (UTC) | | Country | Derived from IP via geo lookup | > [!NOTE] > IP addresses are hashed with a one-way SHA-256 function and never stored in plaintext. Analytics are fully anonymous. --- ## Examples ### Common usage **Download a config file** ``` curl -sSL easy-install.sh/acme/app/config.yaml > config.yaml ``` **Pipe JSON into jq** ``` curl -sSL easy-install.sh/acme/app/defaults.json | jq '.database' ``` **Fetch a Docker Compose file** ``` curl -sSL easy-install.sh/acme/app/docker-compose.yml > docker-compose.yml docker compose up -d ``` **Fetch a Kubernetes manifest** ``` curl -sSL easy-install.sh/acme/app/k8s/deployment.yaml | kubectl apply -f - ``` **Use with wget** ``` wget -qO config.toml easy-install.sh/acme/app/config.toml ``` **Nested paths** ``` curl -sSL easy-install.sh/acme/templates/envs/.env.production > .env ``` **HEAD request to check availability** ``` curl -I easy-install.sh/acme/app/config.yaml ``` Returns headers only (no body). Useful for checking file existence or content type before downloading. --- ## Limitations ### Current limitations > [!WARNING] Public repos only > easy-install.sh can only proxy files from public GitHub repositories. Private repos require authentication that the proxy cannot forward. > [!WARNING] Default branch only > Files are always fetched from the repository's default branch. There is no way to specify a tag, branch, or commit hash in the URL yet. > [!NOTE] Extension-based detection > Content type detection relies on file extensions, not file contents. If your file has no extension or an unusual one, the proxy falls back to `application/octet-stream`. > [!NOTE] 5-minute cache > Responses are cached for 5 minutes. After pushing changes, it may take up to 5 minutes for the proxy to serve the updated file. --- ================ Project Previews ================ ## Project Overview ### Repository page Visit `easy-install.sh/{ns}/{project}` in a browser to see the project overview. ``` https://easy-install.sh/acme/api ``` The page auto-detects what's available for the repo: **Docker image** — Checks GHCR for a `:latest` tag. If found, shows the `docker pull` command. **Install scripts** — Scans the repo root for `.sh`, `.bash`, or `.zsh` files. Shows a `curl | bash` command for the first match. **Config files** — Detects `docker-compose.yml`, `Dockerfile`, `.env`, and similar files. Shows download commands for each. Modes the repo doesn't support yet are shown as grayed-out cards so users can see what's possible. --- ## Container Preview ### Docker tag details Append a tag to see details for a specific image version. ``` https://easy-install.sh/acme/api:v2.1.0 ``` This fetches the manifest from GHCR and shows the digest, content type, and manifest size. If the tag doesn't exist, you'll see an error with troubleshooting hints. --- ## File Preview ### Syntax-highlighted file viewer Add a file path to preview any file in the repo with syntax highlighting. ``` https://easy-install.sh/acme/api/install.sh ``` The preview shows the file contents with the appropriate `curl` command. Scripts get `curl | bash`, other files get `curl -o`. > [!NOTE] > Files over 500 KB are truncated in the preview. The download command always serves the full file. Binary files show a download prompt instead of a code viewer. --- ## Reference ### Preview URLs | URL | Shows | | ---------------------- | ---------------------------------------------------- | | /{ns}/{project} | Project overview with detected features | | /{ns}/{project}:{tag} | Docker tag digest, size, and pull command | | /{ns}/{project}/{path} | Syntax-highlighted file viewer with download command | > [!NOTE] > These preview pages are what browsers see. CLI tools like `docker` and `curl` hit the proxy directly and get the raw content — same URL, different behavior based on the client.