diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d88f02b..1efa2fa 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,56 +1,88 @@ -# Publishes actor-debugger to npm when a version tag is pushed. +# Publishes the npm package in javascript/ to npm, then tags the commit and creates the GitHub +# release. Started manually (Actions -> Publish to npm -> Run workflow) on master; the version it +# publishes is whatever javascript/package.json carries on that commit. # # Release flow: -# npm version patch # bumps package.json, commits, and creates the v* tag -# git push --follow-tags +# 1. Bump the version in javascript/package.json in a PR (e.g. `npm version patch +# --no-git-tag-version` inside javascript/) and merge it to master. +# 2. Actions -> Publish to npm -> Run workflow (branch: master). +# The workflow refuses to run if that version is already on npm or its v* tag already exists, +# so a re-run after a failure is safe. On success it publishes, pushes the vX.Y.Z tag and +# creates a GitHub release with generated notes - never create those by hand. # -# Setup (one-time): create a granular npm access token with read/write on this -# package (or "new packages" for the first publish) and save it as the -# repository secret NPM_TOKEN. -# -# Note: `npm publish --provenance` is not used because it requires a public -# repository. If the repo goes public, add `--provenance` to the publish step -# and `id-token: write` to permissions. +# Uses npm Trusted Publishing (OIDC) - no npm token or repository secret. The trusted publisher +# configured on npmjs.com for the actor-debugger package is: +# repository: apify/actor-debugger +# workflow: publish.yml (this file name must match exactly, including the extension) +# Provenance attestations are generated automatically by trusted publishing, and npm requires +# package.json "repository.url" to point at this exact GitHub repository. name: Publish to npm on: - push: - tags: - - 'v*' + workflow_dispatch: + +# Never let two releases run at once (they would race on the same version/tag). +concurrency: + group: publish-npm + cancel-in-progress: false + +defaults: + run: + working-directory: javascript jobs: check: runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} steps: + - name: Refuse to publish from anything but master + if: github.ref != 'refs/heads/master' + run: | + echo "Publish to npm must be started on master, got ${GITHUB_REF}" >&2 + exit 1 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 - name: Install dependencies run: npm ci - - name: Syntax-check sources - run: | - node --check bin/cli.mjs - node --check lib/debug_server.mjs + - name: Read version from package.json + id: version + run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" - - name: Verify tag matches package.json version + - name: Verify the version is not published yet + env: + VERSION: ${{ steps.version.outputs.version }} run: | - PKG_VERSION="$(node -p "require('./package.json').version")" - TAG_VERSION="${GITHUB_REF_NAME#v}" - if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then - echo "Tag $GITHUB_REF_NAME does not match package.json version $PKG_VERSION" >&2 + # Ask the registry for the published versions (fails loudly on a registry/network error + # rather than silently treating the version as new). + PUBLISHED="$(npm view actor-debugger versions --json)" + if node -e 'process.exit(JSON.parse(process.argv[1]).includes(process.argv[2]) ? 0 : 1)' "$PUBLISHED" "$VERSION"; then + echo "actor-debugger@${VERSION} is already on npm - bump the version in package.json first" >&2 + exit 1 + fi + if git ls-remote --exit-code --tags origin "refs/tags/v${VERSION}" > /dev/null; then + echo "Tag v${VERSION} already exists - bump the version in package.json first" >&2 exit 1 fi + - name: Syntax-check sources + run: | + node --check bin/cli.mjs + node --check lib/debug_server.mjs + node --check lib/inline_sourcemaps.mjs + - name: Smoke test (entrypoint runs and its output passes through) run: | mkdir -p /tmp/smoke && cd /tmp/smoke echo 'console.log("smoke-ok")' > index.js - OUTPUT="$(node "$GITHUB_WORKSPACE/bin/cli.mjs" index.js 2>&1)" + OUTPUT="$(node "$GITHUB_WORKSPACE/javascript/bin/cli.mjs" index.js 2>&1)" echo "$OUTPUT" echo "$OUTPUT" | grep -q 'smoke-ok' @@ -59,7 +91,7 @@ jobs: mkdir -p /tmp/smoke-debug && cd /tmp/smoke-debug echo 'setTimeout(() => console.log("done"), 15000)' > index.js ACTOR_WEB_SERVER_PORT=4321 ACTOR_WEB_SERVER_URL=http://localhost:4321 \ - node "$GITHUB_WORKSPACE/bin/cli.mjs" index.js & + node "$GITHUB_WORKSPACE/javascript/bin/cli.mjs" index.js & CLI_PID=$! for i in $(seq 1 20); do sleep 0.5 @@ -75,19 +107,49 @@ jobs: publish: needs: check runs-on: ubuntu-latest - environment: npm + permissions: + id-token: write # npm Trusted Publishing (OIDC) + contents: write # push the release tag and create the GitHub release + env: + VERSION: ${{ needs.check.outputs.version }} + TAG: v${{ needs.check.outputs.version }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history and tags, so the release notes start at the previous v* tag - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 registry-url: 'https://registry.npmjs.org' + # Trusted publishing needs npm >= 11.5.1; make sure the runner's npm is new enough. + - name: Update npm + run: | + npm install -g npm@latest + npm --version + - name: Install dependencies run: npm ci - - name: Publish + - name: Publish (trusted publishing) run: npm publish + + - name: Tag the release commit + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -a "$TAG" -m "actor-debugger $TAG (npm)" + git push origin "$TAG" + + - name: Create the GitHub release env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + GH_TOKEN: ${{ github.token }} + run: | + # Previous npm release tag (v*, not the Python py-v* tags), if any, for the notes range. + PREV="$(git describe --tags --abbrev=0 --match 'v*' "${TAG}^" 2>/dev/null || true)" + if [ -n "$PREV" ]; then + gh release create "$TAG" --title "actor-debugger $TAG (npm)" --generate-notes --notes-start-tag "$PREV" --verify-tag + else + gh release create "$TAG" --title "actor-debugger $TAG (npm)" --generate-notes --verify-tag + fi diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml index 66fceb7..9305094 100644 --- a/.github/workflows/publish_to_pypi.yml +++ b/.github/workflows/publish_to_pypi.yml @@ -1,53 +1,83 @@ -# Publishes the Python actor-debugger package to PyPI when a py-v* tag is pushed. +# Publishes the Python package in python/ to PyPI, then tags the commit and creates the GitHub +# release. Started manually (Actions -> Publish to PyPI -> Run workflow) on master; the version it +# publishes is whatever python/pyproject.toml carries on that commit. # -# Uses PyPI Trusted Publishing (OIDC) - no API token or secret anywhere. The trusted -# publisher on PyPI is configured as: +# Release flow: +# 1. Bump the version in python/pyproject.toml and python/src/actor_debugger/__init__.py in a +# PR and merge it to master. +# 2. Actions -> Publish to PyPI -> Run workflow (branch: master). +# The workflow refuses to run if that version is already on PyPI or its py-v* tag already +# exists, so a re-run after a failure is safe. On success it publishes, pushes the py-vX.Y.Z tag +# and creates a GitHub release with generated notes - never create those by hand. +# +# Uses PyPI Trusted Publishing (OIDC) - no API token or secret anywhere. The trusted publisher on +# PyPI is configured as: # project: actor-debugger # repository: apify/actor-debugger # workflow: publish_to_pypi.yml (this file name must match exactly) -# -# Release flow: -# (bump version in python/pyproject.toml and python/src/actor_debugger/__init__.py, commit) -# git tag py-vX.Y.Z && git push --tags -# -# Can also be started manually (Actions -> Publish to PyPI -> Run workflow); a manual run -# skips the tag/version check and publishes whatever version pyproject.toml carries on the -# chosen ref (PyPI rejects re-uploads of an existing version, so re-runs are safe). name: Publish to PyPI on: - push: - tags: - - 'py-v*' workflow_dispatch: +# Never let two releases run at once (they would race on the same version/tag). +concurrency: + group: publish-pypi + cancel-in-progress: false + +defaults: + run: + working-directory: python + jobs: check: runs-on: ubuntu-latest - defaults: - run: - working-directory: python + outputs: + version: ${{ steps.version.outputs.version }} steps: + - name: Refuse to publish from anything but master + if: github.ref != 'refs/heads/master' + run: | + echo "Publish to PyPI must be started on master, got ${GITHUB_REF}" >&2 + exit 1 + - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' - - name: Install package - run: pip install . + - name: Read version from pyproject.toml + id: version + run: | + VERSION="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")" + INIT_VERSION="$(python -c "import re; print(re.search(r'__version__\s*=\s*[\"\']([^\"\']+)', open('src/actor_debugger/__init__.py').read()).group(1))")" + if [ "$VERSION" != "$INIT_VERSION" ]; then + echo "pyproject.toml version $VERSION does not match __init__.__version__ $INIT_VERSION" >&2 + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - name: Verify tag matches pyproject version - if: github.ref_type == 'tag' + - name: Verify the version is not published yet + env: + VERSION: ${{ steps.version.outputs.version }} run: | - PKG_VERSION="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")" - TAG_VERSION="${GITHUB_REF_NAME#py-v}" - if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then - echo "Tag $GITHUB_REF_NAME does not match pyproject.toml version $PKG_VERSION" >&2 + # Ask PyPI for the released versions (fails loudly on a network error rather than + # silently treating the version as new). + PUBLISHED="$(curl -sSf https://pypi.org/pypi/actor-debugger/json)" + if python -c "import json,sys; sys.exit(0 if sys.argv[2] in json.loads(sys.argv[1])['releases'] else 1)" "$PUBLISHED" "$VERSION"; then + echo "actor-debugger ${VERSION} is already on PyPI - bump the version first" >&2 + exit 1 + fi + if git ls-remote --exit-code --tags origin "refs/tags/py-v${VERSION}" > /dev/null; then + echo "Tag py-v${VERSION} already exists - bump the version first" >&2 exit 1 fi + - name: Install package + run: pip install . + - name: Smoke test (entrypoint runs and its output passes through) run: | mkdir -p /tmp/smoke && cd /tmp/smoke @@ -75,16 +105,21 @@ jobs: needs: check runs-on: ubuntu-latest permissions: - id-token: write # required for PyPI Trusted Publishing (OIDC) + id-token: write # PyPI Trusted Publishing (OIDC) + contents: write # push the release tag and create the GitHub release + env: + VERSION: ${{ needs.check.outputs.version }} + TAG: py-v${{ needs.check.outputs.version }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history and tags, so the release notes start at the previous py-v* tag - uses: actions/setup-python@v5 with: python-version: '3.11' - name: Build distributions - working-directory: python run: | pip install build python -m build @@ -93,3 +128,22 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: python/dist + + - name: Tag the release commit + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -a "$TAG" -m "actor-debugger $TAG (PyPI)" + git push origin "$TAG" + + - name: Create the GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + # Previous PyPI release tag (py-v*, not the npm v* tags), if any, for the notes range. + PREV="$(git describe --tags --abbrev=0 --match 'py-v*' "${TAG}^" 2>/dev/null || true)" + if [ -n "$PREV" ]; then + gh release create "$TAG" --title "actor-debugger $TAG (PyPI)" --generate-notes --notes-start-tag "$PREV" --verify-tag + else + gh release create "$TAG" --title "actor-debugger $TAG (PyPI)" --generate-notes --verify-tag + fi diff --git a/README.md b/README.md index 732ed91..160ddbd 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,37 @@ # actor-debugger -> **Python Actors:** the same one-link browser debugging exists for Python, injected the same way — -> `RUN pip install actor-debugger` + `CMD ["python3", "-m", "actor_debugger", "--brk"]` — built on -> debugpy plus a served DAP frontend instead of the Node inspector plus Chrome DevTools. -> See [`python/README.md`](python/README.md). - -Drop-in remote debugging for **any Apify Node/TS Actor** with a one-line Dockerfile change. It -launches your Actor under the Node inspector **and serves a full Chrome DevTools UI over the run's -container URL** — so you open one link in your own browser and debug. No wstunnel, no local setup, -no rebuild of your source, and **no browser in the Actor**. - -```dockerfile -# Get the package -RUN npm install actor-debugger - -# pause on the first line until a debugger attaches (for short-lived Actors): -CMD ["npx", "actor-debugger", "--brk"] - -# or point at a specific entry: -CMD ["npx", "actor-debugger", "dist/main.js"] -``` - -## How it works - -1. It resolves your Actor's entrypoint (see detection order below) and launches it as - `node --inspect=127.0.0.1:9229 ` — so the inspector is on **your** code, in its own - process, exactly as it normally runs. -2. It runs one HTTP server on `ACTOR_WEB_SERVER_PORT` that: - - **serves the Chrome DevTools frontend** (static files) so your *local* browser opens it — no - Chrome, Xvfb, or VNC in the Actor; - - **proxies the CDP WebSocket** to the inspector, rewriting `Host` to a loopback IP and dropping - `Origin`. Node's inspector rejects DNS-name hosts (anti-DNS-rebinding), so this rewrite is - what lets a browser reach it over `.runs.apify.net`. -3. The run log prints one URL. Open it in your browser → real DevTools attached to your Actor. - -The DevTools frontend comes from **[`chii`](https://github.com/liriliri/chii)** (MIT), which ships -a *prebuilt* Chrome DevTools frontend. The `chrome-devtools-frontend` npm package is unbuilt -TypeScript source (needs Chromium's GN/ninja toolchain to compile), so it can't be served as-is — -chii's build is the same frontend, ready to serve. - -## Activation - -Running the Actor through `npx actor-debugger` **is** the switch — debugging is on whenever the -`CMD` line above is in place. To turn it off, revert the `CMD` to the Actor's normal entrypoint -(e.g. `CMD ["npm", "start"]`) and rebuild. Pass `--brk` to pause on the first line until a -debugger attaches — useful for Actors that would otherwise finish before you connect. - -## Connect - -Open the URL the run log prints, in your own browser: - -``` -https://.runs.apify.net/devtools/js_app.html?wss=.runs.apify.net/ -``` - -That page **is** Chrome DevTools; it connects to your Actor over the container URL. Set breakpoints -in your sources (via source maps), step, inspect — no `devtools://` URL, no local install. -Prefer the raw channel? `npx wscat -c "wss://.runs.apify.net/"`, or point -`Playwright/Puppeteer connectOverCDP` at that wss URL. - -The WebSocket scheme in the printed URL follows the container URL's scheme: `wss` on the platform -(https container URLs), plain `ws` on a local Apify dev stack (http on localhost). Always use the -URL exactly as printed in the run log. - -## TypeScript sources (automatic) - -A remote DevTools frontend can never fetch `file://` URLs from the container, so external -`.js.map` files — the standard `"sourceMap": true` tsc output — are unreachable to it, and -DevTools would fall back to the generated JS ("Source map failed to load"). The debugger fixes -this itself at startup: it scans the compiled output, reads each external `.map` from the -container's disk, embeds the original TS text into it (`sourcesContent`, read from the `.ts` -files in the image), and rewrites the reference into an inline `data:` URL. Any tsc setup that -emits source maps at all (`sourceMap` or `inlineSourceMap`) therefore just works — no tsconfig -changes needed. - -The one unrecoverable case is a build with no source maps: then the run log prints a hint to -compile with `"sourceMap": true`. If the `.ts` files aren't in the image (a multi-stage build -copying only `dist/`), mappings still inline but sources can't be shown — the log says so; `COPY` -your `src/` into the final stage to fix it. - -## Entrypoint detection order - -1. An explicit path argument, if given. -2. The file in `package.json` `scripts.start` (e.g. `node dist/main.js` → `dist/main.js`). -3. `package.json` `main`. -4. Conventional paths: `dist/main.js`, `dist/index.js`, `build/main.js`, `src/main.js`, `main.js`, `index.js`. +Drop-in remote debugging for Apify Actors: a one-line `CMD` change in the Dockerfile launches the +Actor under a debugger and serves a full debugger UI over the run's container URL. Open one link in +your own browser and debug — no local setup, no tunnel, no rebuild of your code, no browser or IDE +inside the Actor. + +| Runtime | Package | Integration | Details | +|---|---|---|---| +| Node.js / TypeScript | [`actor-debugger` on npm](https://www.npmjs.com/package/actor-debugger) | `RUN npm install actor-debugger` + `CMD ["npx", "actor-debugger", "--brk"]` | [`javascript/README.md`](javascript/README.md) | +| Python | [`actor-debugger` on PyPI](https://pypi.org/project/actor-debugger/) | `RUN pip install actor-debugger` + `CMD ["python3", "-m", "actor_debugger", "--brk"]` | [`python/README.md`](python/README.md) | + +Both variants share the same architecture: your Actor runs under its native debugger (the Node +inspector, or debugpy for Python), and one HTTP server on `ACTOR_WEB_SERVER_PORT` serves the +frontend and bridges the debug protocol to it. The run log prints the URL to open. + +## Repository layout + +- [`javascript/`](javascript/) — the npm package (`bin/`, `lib/`), its README, and a sample TS Actor + in `example-actor/`. +- [`python/`](python/) — the PyPI package (`src/actor_debugger/`) and its README. +- [`.github/workflows/`](.github/workflows/) — release automation. Both workflows are started + manually from the Actions tab on `master`: `publish.yml` publishes the version in + `javascript/package.json` to npm and `publish_to_pypi.yml` the version in `python/pyproject.toml` + to PyPI. Each then creates its tag (`vX.Y.Z` for npm, `py-vX.Y.Z` for PyPI) and GitHub release + itself. Both use registry trusted publishing (OIDC), so there are no publishing secrets in the + repository. ## Security -The debug endpoint is **unauthenticated** — anyone who reaches the container URL and the run's -`uuid` can execute code in your run (and read its env, including `APIFY_TOKEN`). Keep the -`actor-debugger` `CMD` only in builds you are actively debugging, prefer a restricted run/token, -never ship it in a published Actor, and gate the endpoint (owner-only) before any non-prototype -use. - -## Verified - -Tested end-to-end against a **browserless** generic sample TS Actor (`example-actor/`, plain -`apify/actor-node`, no Chrome): - -- The one-line `CMD` detects the entrypoint and runs the Actor under `--inspect`; explicit path - and auto-detect modes both pass. -- `/devtools/js_app.html` and its assets are served (200); `/json` is proxied; a CDP client - round-trips `Runtime.evaluate` through the proxy with browser-like `Host`/`Origin`. -- **Headless Chromium loaded the served DevTools frontend and the Node inspector reported - "Debugger attached"** — i.e. the served Chrome DevTools UI genuinely connects to the Actor with no - browser in the image. - -One thing to confirm on real infrastructure: the frontend's `wss://` connection through Apify's -ingress when the ingress negotiates HTTP/2 (WebSocket-over-h2). See the sibling -`typescript-debug-browser` handoff for the `curl --http1.1` probe that settles it. - -## Releasing - -Publishing to npm is automated by [`.github/workflows/publish.yml`](.github/workflows/publish.yml), -which fires on `v*` tags. To cut a release: - -```bash -npm version patch # or minor / major - bumps package.json, commits, tags vX.Y.Z -git push --follow-tags -``` - -The workflow syntax-checks the sources, smoke-tests both modes (disabled pass-through and the -debug server with `/json/list` + DevTools frontend), verifies the tag matches `package.json` -`version`, and then runs `npm publish`. - -One-time setup: create a granular npm access token with publish rights for this package and save -it as the repository secret `NPM_TOKEN`. The publish job runs in the `npm` GitHub environment, so -you can optionally add required reviewers there to gate releases. +The debug endpoint is unauthenticated: anyone who reaches the container URL can execute code in the +run and read its environment, including `APIFY_TOKEN`. Keep the debugger `CMD` only in builds you are +actively debugging, and never ship it in a published Actor. See each README for details. -## Notes +## License -- Depends on `chii` for the prebuilt DevTools frontend (~16 MB of static assets, no runtime browser). -- Pure Node otherwise — works on any `apify/actor-node*` base image, no Xvfb / Chrome / apt. +Apache-2.0 — see [`LICENSE`](LICENSE). diff --git a/javascript/README.md b/javascript/README.md new file mode 100644 index 0000000..c8691f8 --- /dev/null +++ b/javascript/README.md @@ -0,0 +1,123 @@ +# actor-debugger (Node.js / TypeScript) + +> Looking for Python Actors? The same one-link browser debugging exists for Python, built on debugpy +> plus a served DAP frontend — see +> [`python/README.md`](https://github.com/apify/actor-debugger/blob/master/python/README.md). + +Drop-in remote debugging for **any Apify Node/TS Actor** with a one-line Dockerfile change. It +launches your Actor under the Node inspector **and serves a full Chrome DevTools UI over the run's +container URL** — so you open one link in your own browser and debug. No wstunnel, no local setup, +no rebuild of your source, and **no browser in the Actor**. + +```dockerfile +# Get the package +RUN npm install actor-debugger + +# pause on the first line until a debugger attaches (for short-lived Actors): +CMD ["npx", "actor-debugger", "--brk"] + +# or point at a specific entry: +CMD ["npx", "actor-debugger", "dist/main.js"] +``` + +## How it works + +1. It resolves your Actor's entrypoint (see detection order below) and launches it as + `node --inspect=127.0.0.1:9229 ` — so the inspector is on **your** code, in its own + process, exactly as it normally runs. +2. It runs one HTTP server on `ACTOR_WEB_SERVER_PORT` that: + - **serves the Chrome DevTools frontend** (static files) so your *local* browser opens it — no + Chrome, Xvfb, or VNC in the Actor; + - **proxies the CDP WebSocket** to the inspector, rewriting `Host` to a loopback IP and dropping + `Origin`. Node's inspector rejects DNS-name hosts (anti-DNS-rebinding), so this rewrite is + what lets a browser reach it over `.runs.apify.net`. +3. The run log prints one URL. Open it in your browser → real DevTools attached to your Actor. + +The DevTools frontend comes from **[`chii`](https://github.com/liriliri/chii)** (MIT), which ships +a *prebuilt* Chrome DevTools frontend. The `chrome-devtools-frontend` npm package is unbuilt +TypeScript source (needs Chromium's GN/ninja toolchain to compile), so it can't be served as-is — +chii's build is the same frontend, ready to serve. + +## Activation + +Running the Actor through `npx actor-debugger` **is** the switch — debugging is on whenever the +`CMD` line above is in place. To turn it off, revert the `CMD` to the Actor's normal entrypoint +(e.g. `CMD ["npm", "start"]`) and rebuild. Pass `--brk` to pause on the first line until a +debugger attaches — useful for Actors that would otherwise finish before you connect. + +## Connect + +Open the URL the run log prints, in your own browser: + +``` +https://.runs.apify.net/devtools/js_app.html?wss=.runs.apify.net/ +``` + +That page **is** Chrome DevTools; it connects to your Actor over the container URL. Set breakpoints +in your sources (via source maps), step, inspect — no `devtools://` URL, no local install. +Prefer the raw channel? `npx wscat -c "wss://.runs.apify.net/"`, or point +`Playwright/Puppeteer connectOverCDP` at that wss URL. + +The WebSocket scheme in the printed URL follows the container URL's scheme: `wss` on the platform +(https container URLs), plain `ws` on a local Apify dev stack (http on localhost). Always use the +URL exactly as printed in the run log. + +## TypeScript sources (automatic) + +A remote DevTools frontend can never fetch `file://` URLs from the container, so external +`.js.map` files — the standard `"sourceMap": true` tsc output — are unreachable to it, and +DevTools would fall back to the generated JS ("Source map failed to load"). The debugger fixes +this itself at startup: it scans the compiled output, reads each external `.map` from the +container's disk, embeds the original TS text into it (`sourcesContent`, read from the `.ts` +files in the image), and rewrites the reference into an inline `data:` URL. Any tsc setup that +emits source maps at all (`sourceMap` or `inlineSourceMap`) therefore just works — no tsconfig +changes needed. + +The one unrecoverable case is a build with no source maps: then the run log prints a hint to +compile with `"sourceMap": true`. If the `.ts` files aren't in the image (a multi-stage build +copying only `dist/`), mappings still inline but sources can't be shown — the log says so; `COPY` +your `src/` into the final stage to fix it. + +## Entrypoint detection order + +1. An explicit path argument, if given. +2. The file in `package.json` `scripts.start` (e.g. `node dist/main.js` → `dist/main.js`). +3. `package.json` `main`. +4. Conventional paths: `dist/main.js`, `dist/index.js`, `build/main.js`, `src/main.js`, `main.js`, `index.js`. + +## Security + +The debug endpoint is **unauthenticated** — anyone who reaches the container URL and the run's +`uuid` can execute code in your run (and read its env, including `APIFY_TOKEN`). Keep the +`actor-debugger` `CMD` only in builds you are actively debugging, prefer a restricted run/token, +never ship it in a published Actor, and gate the endpoint (owner-only) before any non-prototype +use. + +## Releasing + +Publishing to npm is done by +[`.github/workflows/publish.yml`](https://github.com/apify/actor-debugger/blob/master/.github/workflows/publish.yml), +started manually from the Actions tab. It publishes via **npm Trusted Publishing** (OIDC) — no npm +token or repository secret, and provenance attestations are generated automatically. The trusted +publisher configured on npmjs.com is: repository `apify/actor-debugger`, workflow `publish.yml` +(the workflow file name must stay exactly that). To cut a release: + +1. Bump the version in `javascript/package.json` in a PR and merge it to `master`: + + ```bash + cd javascript + npm version patch --no-git-tag-version # or minor / major + ``` + +2. Open **Actions → Publish to npm → Run workflow** on `master`. + +The workflow refuses to run on any other branch, or if that version is already on npm or its +`vX.Y.Z` tag already exists. It then syntax-checks the sources, smoke-tests both modes (disabled +pass-through and the debug server with `/json/list` + DevTools frontend), checks the pack contents, +runs `npm publish` from `javascript/`, and finally **pushes the `vX.Y.Z` tag and creates the GitHub +release** with generated notes. Do not create tags or releases by hand. + +## Notes + +- Depends on `chii` for the prebuilt DevTools frontend (~16 MB of static assets, no runtime browser). +- Pure Node otherwise — works on any `apify/actor-node*` base image, no Xvfb / Chrome / apt. diff --git a/bin/cli.mjs b/javascript/bin/cli.mjs similarity index 100% rename from bin/cli.mjs rename to javascript/bin/cli.mjs diff --git a/example-actor/.actor/actor.json b/javascript/example-actor/.actor/actor.json similarity index 100% rename from example-actor/.actor/actor.json rename to javascript/example-actor/.actor/actor.json diff --git a/example-actor/.gitignore b/javascript/example-actor/.gitignore similarity index 100% rename from example-actor/.gitignore rename to javascript/example-actor/.gitignore diff --git a/example-actor/Dockerfile b/javascript/example-actor/Dockerfile similarity index 100% rename from example-actor/Dockerfile rename to javascript/example-actor/Dockerfile diff --git a/example-actor/package.json b/javascript/example-actor/package.json similarity index 100% rename from example-actor/package.json rename to javascript/example-actor/package.json diff --git a/example-actor/src/main.ts b/javascript/example-actor/src/main.ts similarity index 100% rename from example-actor/src/main.ts rename to javascript/example-actor/src/main.ts diff --git a/example-actor/tsconfig.json b/javascript/example-actor/tsconfig.json similarity index 100% rename from example-actor/tsconfig.json rename to javascript/example-actor/tsconfig.json diff --git a/lib/debug_server.mjs b/javascript/lib/debug_server.mjs similarity index 100% rename from lib/debug_server.mjs rename to javascript/lib/debug_server.mjs diff --git a/lib/inline_sourcemaps.mjs b/javascript/lib/inline_sourcemaps.mjs similarity index 100% rename from lib/inline_sourcemaps.mjs rename to javascript/lib/inline_sourcemaps.mjs diff --git a/package-lock.json b/javascript/package-lock.json similarity index 99% rename from package-lock.json rename to javascript/package-lock.json index e5f48fe..fe60d11 100644 --- a/package-lock.json +++ b/javascript/package-lock.json @@ -1,12 +1,12 @@ { "name": "actor-debugger", - "version": "0.1.2", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "actor-debugger", - "version": "0.1.2", + "version": "0.1.5", "license": "Apache-2.0", "dependencies": { "chii": "^1.14.0" diff --git a/package.json b/javascript/package.json similarity index 75% rename from package.json rename to javascript/package.json index 4f573ff..be14c55 100644 --- a/package.json +++ b/javascript/package.json @@ -1,6 +1,6 @@ { "name": "actor-debugger", - "version": "0.1.4", + "version": "0.1.5", "description": "Drop-in remote debugger for Apify Node/TS Actors: one Dockerfile CMD line launches your Actor under the Node inspector, reachable over the run's container URL. No local setup, no rebuild of your own code.", "type": "module", "bin": { @@ -16,12 +16,13 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/Pijukatel/actor-debugger.git" + "url": "git+https://github.com/apify/actor-debugger.git", + "directory": "javascript" }, "bugs": { - "url": "https://github.com/Pijukatel/actor-debugger/issues" + "url": "https://github.com/apify/actor-debugger/issues" }, - "homepage": "https://github.com/Pijukatel/actor-debugger#readme", + "homepage": "https://github.com/apify/actor-debugger/tree/master/javascript#readme", "keywords": [ "apify", "actor", diff --git a/python/README.md b/python/README.md index 02e038c..3f8cde8 100644 --- a/python/README.md +++ b/python/README.md @@ -1,13 +1,25 @@ # actor-debugger (Python) +**An in-browser Python debugger for the Apify platform.** Set breakpoints, step through your Actor, +walk the call stack, inspect variables and evaluate expressions in the paused frame — in a browser +tab, straight from the run detail in Apify Console. No IDE, no tunnel, no SSH, nothing to install +on your machine. + +![Actor Debugger paused on a breakpoint in a Python Actor, shown in the Live view tab of a run in Apify Console: sources, call stack, variables and an evaluate prompt](https://raw.githubusercontent.com/apify/actor-debugger/master/python/docs/apify-console-live-view.jpg) + +*A Crawlee `BeautifulSoupCrawler` Actor paused on `await context.push_data(data)` in the **Live view** +tab of its run in Apify Console. Sources on the left, call stack and locals on the right (the scraped +`data` dict included), an evaluate prompt for the selected frame at the bottom, and Resume / Over / +Into / Out plus "just my code" in the toolbar. Everything you see runs in the browser.* + Drop-in remote debugging for **any Apify Python Actor** with a two-line Dockerfile change. It launches your Actor under **debugpy** (the debugger that powers VS Code's Python debugging) **and serves a full browser debugger UI over the run's container URL** — so you open one link in your own browser and debug. No IDE, no tunnel, no local setup, no rebuild of your source, and **no browser or IDE inside the Actor**. -This is the Python sibling of the Node/TS [`actor-debugger`](../README.md) npm package, injected -the same way: +This is the Python sibling of the Node/TS [`actor-debugger`](https://github.com/apify/actor-debugger/blob/master/javascript/README.md) +npm package, injected the same way: ```dockerfile # Get the package @@ -29,7 +41,7 @@ Open it: click line numbers to set breakpoints, step, inspect the call stack and evaluate expressions in the paused frame, break on exceptions. `--brk` pauses the Actor on its first line until you attach — drop it to let the Actor run and attach mid-flight instead. -Until the package is published to PyPI, install it from the repository instead: +To try unreleased changes, install straight from the repository instead: ```dockerfile RUN pip install "actor-debugger @ git+https://github.com/apify/actor-debugger.git@master#subdirectory=python" @@ -84,16 +96,6 @@ used exactly as-is: the standard container web-server port is the only channel. channel? `wss://.runs.apify.net/dap` speaks DAP directly, one JSON message per WebSocket text frame — any DAP client can drive it. -## Verified on the Apify platform - -The full loop has been exercised against a real platform run: the UI served over -`https://.runs.apify.net/ui/`, the `wss://…/dap` WebSocket passed the platform ingress, and -a live run was attached, paused, and driven from a plain browser. Automated coverage in this -repo: a protocol test drives a complete DAP session through the bridge (breakpoint, stack, -scopes, variables, evaluate, resume, re-attach after disconnect), and a headless-Chromium test -drives the real UI end to end (entry pause under `--brk`, gutter breakpoints, stepping, -variables, in-frame evaluate, resume, re-attach after page reload), including idle sessions. - ## Security The debug endpoint is **unauthenticated** — anyone who reaches the container URL can execute code @@ -125,22 +127,22 @@ bridge and frontend; only the adapter spawn command and entrypoint detection dif ## Releasing -Publishing to PyPI is automated by -[`.github/workflows/publish_to_pypi.yml`](../.github/workflows/publish_to_pypi.yml), which fires -on `py-v*` tags and publishes via **PyPI Trusted Publishing** (OIDC) — no API token or repository -secret. The trusted publisher configured on PyPI is: project `actor-debugger`, repository -`apify/actor-debugger`, workflow `publish_to_pypi.yml` (the workflow file name must stay exactly -that). To cut a release: - -```bash -# bump version in python/pyproject.toml and python/src/actor_debugger/__init__.py, commit, then: -git tag py-v0.1.0 -git push --tags -``` - -The workflow first installs the package, checks the tag matches the pyproject version, and smoke -tests the CLI and the served debugger UI; only then does the publish job build sdist+wheel and -upload. +Publishing to PyPI is done by +[`.github/workflows/publish_to_pypi.yml`](https://github.com/apify/actor-debugger/blob/master/.github/workflows/publish_to_pypi.yml), +started manually from the Actions tab. It publishes via **PyPI Trusted Publishing** (OIDC) — no API +token or repository secret. The trusted publisher configured on PyPI is: project `actor-debugger`, +repository `apify/actor-debugger`, workflow `publish_to_pypi.yml` (the workflow file name must stay +exactly that). To cut a release: + +1. Bump the version in `python/pyproject.toml` and `python/src/actor_debugger/__init__.py` in a + PR and merge it to `master`. +2. Open **Actions → Publish to PyPI → Run workflow** on `master`. + +The workflow refuses to run on any other branch, if the two version strings disagree, or if that +version is already on PyPI or its `py-vX.Y.Z` tag already exists. It then installs the package, +smoke tests the CLI and the served debugger UI, builds sdist+wheel, uploads them, and finally +**pushes the `py-vX.Y.Z` tag and creates the GitHub release** with generated notes. Do not create +tags or releases by hand. ## Notes diff --git a/python/docs/apify-console-live-view.jpg b/python/docs/apify-console-live-view.jpg new file mode 100644 index 0000000..7d5e680 Binary files /dev/null and b/python/docs/apify-console-live-view.jpg differ diff --git a/python/pyproject.toml b/python/pyproject.toml index a0189c4..3fffd81 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,10 +4,11 @@ build-backend = "hatchling.build" [project] name = "actor-debugger" -version = "0.1.1" +version = "0.1.2" description = "Drop-in remote debugging for Apify Python Actors: debugpy plus a browser debugger UI served over the run's container URL." readme = "README.md" license = { text = "Apache-2.0" } +authors = [{ name = "Apify Technologies s.r.o.", email = "support@apify.com" }] requires-python = ">=3.9" dependencies = ["debugpy>=1.8"] keywords = ["apify", "actor", "debugger", "debugpy", "dap", "remote-debugging"] @@ -19,7 +20,9 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/apify/actor-debugger" +Homepage = "https://github.com/apify/actor-debugger/tree/master/python#readme" +Repository = "https://github.com/apify/actor-debugger" +"Issue Tracker" = "https://github.com/apify/actor-debugger/issues" [project.scripts] actor-debugger = "actor_debugger.cli:main" diff --git a/python/src/actor_debugger/__init__.py b/python/src/actor_debugger/__init__.py index cad7c49..dc4d0d6 100644 --- a/python/src/actor_debugger/__init__.py +++ b/python/src/actor_debugger/__init__.py @@ -1,3 +1,3 @@ """actor-debugger: browser-based remote debugging for Apify Python Actors.""" -__version__ = "0.1.1" +__version__ = "0.1.2"