Skip to content

Try apify-cli in the browser: PR preview builds + a hosted sandbox #1357

Description

@l2ysho

Important

TL;DR — this is experimental idea of mine (POC tried only locally) to have more straightforward way how to test features in PRs. 2 Phases:

  • Phase 1 (can be standalone) -> use https://pkg.pr.new to build preview version per PR, so review do not need to clone and build by himself and add published url (or install comand) as a comment to PR.
  • Phase 2 (this depends on Phase 1)-> use webcontainer (or other solution) to start apify-cli in browser with version published by https://pkg.pr.new. It could be standby actor with apify console auth enabled (or public?) -> apify-cli-preview.apify.actor/?pkg=https://pkg.pr.new/apify-cli@<pr-number> some corner cases would be problem to emulate in browser env, but for general check/verify of changes this could be useful.

This is really just experiment and RFC in one, lot of open questions, feel free to criticize.

Screen.Recording.2026-08-27.at.12.02.29.mov

Motivation

Trying a change to apify-cli today means cloning the repo, installing, and linking
the binary. That is a high bar for two groups we care about:

  • Reviewers — verifying a PR's actual CLI behaviour, not just reading the diff.
  • Users — reproducing a bug report, or trying a fix before it ships to npm.

Both are solved by the same pair of pieces: a per-commit npm package, and a page
that installs an arbitrary package spec into a browser-based Node environment.

Splitting into two phases because phase 1 is independently useful — it needs no
hosting, no headers, and no licensing decision.


Phase 1 — per-PR preview packages via pkg.pr.new

Publish an installable package for every PR commit, so anyone can try a change
without a local checkout.

Opt-in by label. A preview build is published only while the PR carries a
cli-preview label — not for every PR. Reasons:

  • Most PRs (docs, chores, refactors) do not need a package; publishing for all of
    them is noise in the PR thread and wasted CI minutes.
  • The label is a maintainer action, so it doubles as a human gate before we build
    anything from a fork's branch.
  • Whoever wants a preview asks for it, which keeps the signal meaningful.

This is the same shape vite uses for its preview releases (label-gated, plain
pull_request, works for fork contributors).

Scope

  • One-time setup: install the pkg.pr.new GitHub App
    on apify/apify-cli (needs an org admin). No npm token, no repo secrets.
  • Workflow on pull_request (labeled, synchronize), with the job guarded on
    the label being present:
    if: contains(github.event.pull_request.labels.*.name, 'cli-preview')
    labeled covers "add the label to an existing PR"; synchronize republishes on
    each subsequent push while the label stays on. Note labeled fires once per
    label added — adding several labels at once starts several runs; the guard
    keeps the extras from publishing.
  • Build, then publish via pnpm exec pkg-pr-new publish — the CLI packs the
    tree, it does not build it, so the workflow runs pnpm run build first.
  • The install-line PR comment comes for free: pkg.pr.new's default
    (--comment update) posts one comment per PR and edits it in place on each
    push. No comment step to write, and it works on fork PRs because the comment
    is posted by the App, not by the workflow's token.
  • Decide whether removing the label should stop future publishes only (simple) or
    also edit the comment to say the preview is stale (nicer). Suggest starting with
    the former.

Result — a reviewer labels the PR, then runs one command:

npx https://pkg.pr.new/apify-cli@<pr-number>

(Compact form; @<sha> and @<branch> also work, and the long form is
https://pkg.pr.new/apify/apify-cli/apify-cli@<sha>.) npx runs the bin matching
the package name, i.e. the apify entrypoint; a reviewer who wants both apify
and actor on their PATH should npm i -g the same URL instead.

Previews are temporary: pkg.pr.new deletes builds not downloaded for a month, and
everything after six months. Fine for PR review; do not link them from docs.

Acceptance criteria

  • Adding the cli-preview label to an open PR publishes a build and posts the
    install command.
  • Pushes to a labelled PR republish and update the same comment with the new SHA.
  • An unlabelled PR publishes nothing.
  • The cli-preview label exists in the repo with a description saying what it does.
  • A labelled fork PR publishes and gets the comment (expected to work — see Risks).

Phase 2 — hosted browser sandbox on GitHub Pages

A static page that boots a WebContainer (Node in the
browser, via WASM), installs apify-cli, and drops the visitor into a terminal.
Takes the package spec as a query parameter, so it composes with phase 1:

https://apify.github.io/apify-cli-sandbox/?pkg=https://pkg.pr.new/apify-cli@<pr-number>

With no parameter it installs the latest published release — a permanent
"try apify-cli, zero install" link for the docs and the README.

Scope

  • Static page: boot WebContainer, mount a package.json, run npm install, attach
    an xterm terminal to the container shell. (Working prototype exists, ~130 lines,
    no build step, no dependencies.)
  • coi-serviceworker to supply the COOP/COEP headers, since GitHub Pages cannot
    set headers. Prior art: webcontainers-ghpages
    (a small third-party demo, not a StackBlitz project — but the technique is exactly this).
  • Pages deploy workflow.
  • Unsupported-browser message.

Not enabled on forks. The Pages deploy runs only from the base repo:

  • The deploy workflow triggers on push to the default branch, never on
    pull_request, and is guarded on the repository so that forks of this repo do
    not try to publish to their own Pages site:
    if: github.repository == '<org>/<repo>'
  • No sandbox link is posted on fork PRs. A maintainer who wants to try a fork's
    change labels the PR (phase 1), which produces both the package and a usable
    sandbox link.

This is a deliberate limitation, not an oversight: it keeps untrusted branches out
of anything that holds a Pages deploy token.

Explicitly out of scope: one deployed page per PR. The ?pkg= parameter makes
it unnecessary — one deploy serves every PR, and there is nothing to clean up when
a PR closes. A page-per-PR variant would need a gh-pages branch (the Pages
artifact deploy replaces the whole site) plus a teardown job on pull_request: closed, and it does not work for fork PRs without pull_request_target.

Acceptance criteria

  • Page boots and installs the latest release in a supported browser.
  • ?pkg= override installs a pkg.pr.new build from an open PR.
  • apify --help runs in the terminal.
  • Unsupported browsers get a clear message instead of a raw npm error.
  • No deploy is triggered by a fork, and forks of the repo cannot publish.
  • Licensing question resolved before the page goes public (see Risks).

Verified during prototyping

Recorded so nobody re-derives it:

  • Safari does not work in practice. StackBlitz lists Safari 16.4+ as
    supported in beta, but
    npm install inside the container fails on current Safari (observed on 18.6) with
    ERR_INVALID_PROTOCOL: Protocol "https:" not supported. Expected "http:" — an
    unrelated-looking error that costs an hour to diagnose. Same symptom reported in
    webcontainer-core#1983,
    open with no maintainer response. Full support is Chromium and Firefox. Hence
    the unsupported-browser message above.
  • Cookie blockers break WebContainersdocumented by StackBlitz.
  • CORS is not an obstacle, but it is the load-bearing assumption. WebContainer
    networking is browser fetch under the hood — no raw TCP, every outbound request
    is CORS-bound. All three endpoints the sandbox needs send
    access-control-allow-origin: * today: registry.npmjs.org, pkg.pr.new, and
    api.apify.com. If the API ever drops that header, the sandboxed CLI silently
    loses the platform.
  • Buffer npm's output, do not stream it. The install spinner renders as
    hundreds of \ | / - lines in xterm and buries real errors.

Risks / open questions

  • Licensing. StackBlitz requires a license for production use of the
    WebContainer API in a commercial, for-profit setting
    their wording covers use "to meet the needs of your customers, prospective
    customers, and/or employees", which a public Apify sandbox plausibly is.
    Prototypes and POCs are explicitly exempt, so the current prototype is fine.
    Needs an answer before a public deploy. Blocks phase 2 only.
  • Fork PRs — smaller than it looks. pkg-pr-new publish needs no secrets and
    never touches the workflow's read-only GITHUB_TOKEN; the PR comment is posted
    by the pkg.pr.new App, which holds pull-requests write on the base repo. So the
    plain pull_request trigger works for fork PRs — no pull_request_target, no
    workflow_run follow-up. What remains is GitHub's own gate: first-time
    contributors' workflow runs need maintainer approval, and the cli-preview
    label is a second deliberate human gate on top. Phase 2 sidesteps forks
    entirely by not deploying from them (see above).
  • Alternatives evaluated, none viable today. OpenWebContainer (npm support
    still on the roadmap), Nodepod (MIT + Commons Clause; commercial use restricted),
    Nodebox (no sockets to external IPs, so the CLI cannot reach api.apify.com), and
    userland.run/nano. Nano runs the real Node 25 RISC-V binary and works, but in
    our measurements booted 52× slower and computed 110× slower than host Node, plus
    a ~68 MB binary download — unusable for an oclif CLI. Worth re-checking in
    ~6 months.

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    t-buildersIssues owned by the Builders team.

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions