Skip to content
Draft
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
233 changes: 233 additions & 0 deletions .claude/skills/new-package/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
---
name: new-package
description: 'Use when adding a new publishable package under packages/ in this monorepo. Scaffolds it from the ID packages, wires it into the test, artifact and release workflows, publishes a 0.0.0 placeholder to npm or PyPI, and prompts the author to register the GitHub Actions OIDC trusted publisher so the first real release can publish from CI without a token.'
---

# New package

Every package here is published by `changeset publish` from `publish_packages.yml`
over GitHub OIDC trusted publishing: npm packages directly, PyPI packages through
the package's `postPublish` script (`uv publish --trusted-publishing always`). No
registry token exists in CI, so a package whose trusted publisher is not registered
fails at its first release, after the version bump has already been committed and
tagged on `main`.

A trusted publisher can only be attached to a package that already exists on the
registry, so the package name is claimed up front with a `0.0.0` placeholder. The
repo then versions the package from `0.0.0` with an "Initial release" changeset, and
the first CI release mints `0.1.0` with no manual step. `@e2b/id` and `e2b-id` are
the reference: both registries hold a `0.0.0` placeholder with the trusted publisher
attached.

Do the steps in order. Step 4 publishes to a public registry and step 5 needs the
author in a browser, so neither can be undone or finished by the agent alone.

## 1. Pick the names

| Registry | Package name | Workspace name (`package.json`) | Directory |
| -------- | ------------- | ------------------------------- | ------------------------ |
| npm | `@e2b/<name>` | `@e2b/<name>` | `packages/js-<name>` |
| PyPI | `e2b-<name>` | `@e2b/python-<name>` (private) | `packages/python-<name>` |

The Python workspace entry is a `"private": true` `package.json` whose only job is to
let changesets version and tag the package; `publish_selected.cjs` runs its
`postPublish` script for the upload, and `changeset publish` skips it on npm. The
Python import name is `e2b_<name>`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Hyphenated names break Python imports

For a package named e2b-foo-bar, e2b_<name> becomes e2b_foo-bar. That is not a valid Python import name, so the scaffolded package cannot be imported as instructed.

Learn more

PyPI distribution names can contain hyphens, but Python import identifiers cannot. The skill also uses the raw suffix in its placeholder module directory and build configuration. That produces a wheel with a module name that users cannot write as a standard Python import.

Example: For e2b-foo-bar, the prescribed module name is e2b_foo-bar, rather than importable e2b_foo_bar.

Recommended fix: Specify that every hyphen in <name> is converted to an underscore for the Python import, module directory, and module-name setting, while keeping hyphens in the PyPI distribution name.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.


Check both names are free before anything else; a taken name means a different name,
not a different owner:

```sh
npm view @e2b/<name> version # must fail with E404
curl -sI https://pypi.org/pypi/e2b-<name>/json | head -1 # must be 404
```

## 2. Scaffold from the ID packages

Copy the nearest sibling rather than writing from scratch. `packages/js-id` and
`packages/python-id` are the smallest complete examples; a package that talks to the
API should copy `packages/js-sdk` / `packages/python-sdk` conventions instead.

JS (`packages/js-<name>`): `package.json` with `"version": "0.0.0"`,
`publishConfig.access: public`, `repository.directory`, `files`, `exports`, the
`build` / `test` / `typecheck` / `lint` / `format` scripts and `prepublishOnly: pnpm
build`; `tsconfig.json`, `tsdown.config.ts`, `vitest.config.mts`, `LICENSE`,
`README.md`, `.gitignore`. Use `catalog:` versions from `pnpm-workspace.yaml`.

Python (`packages/python-<name>`): `pyproject.toml` with `version = "0.0.0"`, the
`uv_build` backend and `[tool.uv.build-backend] module-name`; `package.json` with
`"private": true`, `"version": "0.0.0"` and the `postVersion` / `lock` /
`postPublish` / `test` / `typecheck` / `lint` / `format` scripts exactly as in
`packages/python-id/package.json`; `Makefile`, `pytest.ini`, `LICENSE`, `README.md`,
and `uv lock` to produce `uv.lock`. Add any new cache directory to the root
`.gitignore` (python-id added `.ruff_cache/`).

Then `pnpm install` so `pnpm-lock.yaml` picks the workspace up, and add the
changeset that turns the placeholder into the first real version:

```md
---
'@e2b/<name>': minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Python package never receives initial release

When adding both packages, the changeset omits @e2b/python-<name> from the release plan. publish_selected.cjs uploads only selected Python workspaces, leaving PyPI at its placeholder version.

Learn more

The Python workspace has its own Changesets identity. A changeset naming only the npm workspace cannot bump its version from 0.0.0 or select its postPublish script. publish_selected selects Python uploads from the release plan, while the reference Python changeset names @e2b/python-id separately.

Example: Adding @e2b/foo and e2b-foo with the shown changeset releases @e2b/foo@0.1.0; e2b-foo stays at 0.0.0 on PyPI.

Recommended fix: Show separate minor changeset entries for @e2b/<name> and @e2b/python-<name> when both packages are added, and specify the appropriate single entry for one-registry packages.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

---
Comment on lines +68 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include the Python package in the initial release plan

When this skill scaffolds both packages, the shown changeset schedules only @e2b/<name>. Consequently changeset version leaves @e2b/python-<name> at 0.0.0, and publish_selected.cjs does not select its postPublish script, so the first release publishes only the JavaScript package while the PyPI project remains a placeholder. Add the Python workspace to this changeset or instruct authors to create a separate Python changeset, as the existing ID packages do.

Useful? React with 👍 / 👎.


Initial release: <one line on what the package is>.
```
Comment on lines +66 to +74

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) An agent following the skill writes no changeset for a new Python package, so e2b-<name> is never versioned or released past the 0.0.0 placeholder. The sample changeset only names '@ e2b/<name>'. The skill's own table gives the Python workspace the different name @ e2b/python-<name>. For a Python-only package, '@ e2b/<name>' matches no workspace at all. Fix: show one changeset entry per workspace being added, like the two repo files .changeset/js-id-package.md ('@ e2b/id': minor) and .changeset/python-id-package.md ('@ e2b/python-id': minor).

Why this was flagged

The skill's step 1 table (SKILL.md:26-29) names the Python workspace @ e2b/python-<name> (private), and changesets versions it through privatePackages.version: true in .changeset/config.json. Step 2 (SKILL.md:66-74) gives only one sample changeset, keyed '@ e2b/<name>', which is the npm package. An agent adding both packages bumps only the JS one. publish_selected.cjs:16-25 runs postPublish only for packages selected for release, so the Python package stays at 0.0.0 and never uploads. An agent adding only a Python package writes a changeset naming a package that does not exist, and changesets rejects it. The ID reference this skill copies used two separate files, .changeset/js-id-package.md and .changeset/python-id-package.md, each keyed by its own workspace name.

Verification: SKILL.md:30 names the Python workspace @ e2b/python-<name>, but the only sample changeset, at lines 68-71, is keyed '@ e2b/<name>': minor. .changeset/config.json has privatePackages.version: true, and .github/scripts/publish_selected.cjs runs postPublish only when selected.has(pkg.name) && pkg.scripts?.postPublish. A Python package with no changeset of its own therefore stays at 0.0.0.


A `minor` bump from `0.0.0` releases `0.1.0`. Never use `major`.

## 3. Wire it into CI and the release

Each file below names every package explicitly. Grep for `js-id` / `python-id` /
`@e2b/id` / `@e2b/python-id` and add the new package next to each hit:

- `.github/workflows/<name>_tests.yml`: a `workflow_call` test workflow, modelled on
`id_tests.yml` (no secrets) or `js_sdk_tests.yml` / `python_sdk_tests.yml`
(needs `E2B_API_KEY`).
- `.github/workflows/sdk_tests.yml`: a path filter under `changes` and a job that
`uses:` the new test workflow.
Comment on lines +80 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) If an agent follows this skill, the new package's PR tests can be skipped silently on every PR, or fail without turning the required check red. The sdk_tests.yml step asks only for a path filter and a uses: job. It leaves out the changes.outputs entry and the status.needs entry. The suggested grep (js-id / python-id / @ e2b/id / @ e2b/python-id) matches neither line. Fix: list all four sdk_tests.yml edits, including <name>: ${{ steps.filter.outputs.<name> }} under changes.outputs and the new job under status.needs, and point the grep at id-tests and outputs.id too.

Why this was flagged

In .github/workflows/sdk_tests.yml, a job reads a filter result only through the changes job's outputs: map (line 38: id: ${{ steps.filter.outputs.id }}). A filter added under filters: with no entry in that map leaves needs.changes.outputs.<name> empty. The new job's if: is then false on every pull_request event, so it only runs on workflow_dispatch. The status aggregator (its needs: list ends with - id-tests) fails only when a listed job failed or was cancelled. A skipped job, or one left out of needs, therefore still gives a green 'SDK Tests Status'. SKILL.md:86-87 asks only for 'a path filter under changes and a job that uses: the new test workflow'. The grep at SKILL.md:80-81 matches only the packages/js-id/** and packages/python-id/** filter lines (sdk_tests.yml:118-119), not id: in outputs or id-tests in status.needs. A package scaffolded this way can merge with broken tests that never run or never block. release.yml then runs its tests for the first time at release.

Verification: SKILL.md step 3 asks only for "a path filter under changes and a job that uses: the new test workflow." In .github/workflows/sdk_tests.yml, a filter with no changes.outputs entry leaves the job condition false on every pull_request. A job missing from status.needs can fail without turning "SDK Tests Status" red. The suggested grep matches neither line.

Comment on lines +86 to +87

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 New package failures escape required test status

When the new test job fails, status can still pass because the instructions omit that job from status.needs. The required SDK test check then reports success without the new package passing.

Learn more

The SDK test workflow has an aggregate status job that gates PRs. Its result check sees only jobs listed under status.needs, not every job in the workflow. Adding a new package test job without adding that dependency lets the aggregate check pass when that suite fails.

Example: The new foo-tests job runs for a PR and fails. Other suites pass or skip, and status sees no failure in its needs collection, so SDK Tests Status succeeds.

Recommended fix: Add the new test job to status.needs as well as creating its path filter and job. Verify a failing new-package test causes the aggregate check to fail.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

- `.github/workflows/release.yml`: a "Check … Release" step with its output in
`preflight`, a test job gated on that output, and the job added to both
`publish.needs` and `report-failure.needs`. A package missing from `publish.needs`
publishes untested; one missing from `report-failure.needs` fails silently.
- `.github/scripts/build_release_itinerary.cjs`: a label, so the Slack release
message names the package.
- `.github/workflows/pkg_artifacts.yml`: a pack/build step, an upload step, and a
line in the PR comment body, so reviewers get a preview artifact.
- `.github/workflows/lint.yml` and `typecheck.yml` (Python only): a
`uv sync --locked` step for the new directory.

Run `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` and `pnpm run test`
before moving on.

## 4. Publish the 0.0.0 placeholder

Publish from a temp directory, never from the repo: the real `0.0.0` must stay
unpublished so the first CI release is the first real artifact. The placeholder
carries no code, and its description says so.

npm. The account must belong to the `e2b` npm org with publish rights; `npm whoami`
shows it. `npm publish` prompts for the 2FA code when the account requires one.

```sh
dir=$(mktemp -d) && cd "$dir"
cat > package.json <<'EOF'
{
"name": "@e2b/<name>",
"version": "0.0.0",
"description": "Placeholder for <description>. The first real release follows shortly.",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/e2b-dev/e2b",
"directory": "packages/js-<name>"
},
"publishConfig": { "access": "public" }
}
EOF
printf '# @e2b/<name>\n\nPlaceholder. The first real release follows shortly.\n' > README.md
npm whoami
npm publish --access public
npm view @e2b/<name> version # 0.0.0
```

PyPI. A project that does not exist yet cannot have a project-scoped token, so this
needs an account-scoped API token from the account that owns the other `e2b-*`
projects. Read it into `UV_PUBLISH_TOKEN` from a password manager; never paste it
on the command line or echo it. The placeholder is a wheel only, like `e2b-id`.

```sh
dir=$(mktemp -d) && cd "$dir"
mkdir e2b_<name>
printf '"""Placeholder for <description>. The first real release follows shortly."""\n' > e2b_<name>/__init__.py
cat > pyproject.toml <<'EOF'
[project]
name = "e2b-<name>"
version = "0.0.0"
description = "Placeholder for <description>. The first real release follows shortly."
authors = [{ name = "e2b", email = "hello@e2b.dev" }]
license = "Apache-2.0"
requires-python = ">=3.10"
dependencies = []

[build-system]
requires = ["uv_build>=0.10.0,<0.11.0"]
build-backend = "uv_build"

[tool.uv.build-backend]
module-name = ["e2b_<name>"]
module-root = ""
EOF
uv build --wheel
# The author exports UV_PUBLISH_TOKEN in their own shell beforehand.
uv publish dist/*.whl
Comment on lines +133 to +162

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) Authors following the PyPI placeholder step leave a token that can publish any e2b-* project, including e2b, in the agent session. The skill never says to revoke it afterwards. The agent's uv publish dist/*.whl (SKILL.md:162) only sees UV_PUBLISH_TOKEN if it is exported in the environment that launched the agent. Every later subprocess inherits it, and line 135 implies a token kept in a password manager. Fix: make the token-free pending-publisher route the default. Otherwise use a token minted for this one upload, have the author run uv publish in their own terminal, and delete the token right after.

Why this was flagged

Step 4 (SKILL.md:133-136) tells the author to get an account-scoped token "from the account that owns the other e2b-* projects" and read it into UV_PUBLISH_TOKEN from a password manager. The agent then runs uv publish dist/*.whl itself (SKILL.md:161-162). For that call to see the token, the author must export it in the shell that starts Claude Code. From then on the token is in the env of every command the session runs: pnpm install lifecycle scripts, tests, and npm publish. Nothing in the skill says to scope it to this upload or delete it on PyPI afterwards. An account-scoped token can upload new releases of e2b, e2b-code-interpreter and e2b-desktop. Before this change there was no documented flow that put such a token in front of an agent. The "never paste it on the command line or echo it" warning does not help, because the exposure comes from the env inheritance itself. The pending-publisher alternative at SKILL.md:166-169 avoids the token completely but is offered only as an opt-out.

Verification: SKILL.md:133-135 asks for an account-scoped token read into UV_PUBLISH_TOKEN, and line 162 runs uv publish dist/*.whl. For line 162 to see the token, it has to be exported in the environment that started the session. No line says to unset it after publishing or revoke the account-wide token.

curl -s https://pypi.org/pypi/e2b-<name>/json | python3 -c 'import json,sys; print(json.load(sys.stdin)["info"]["version"])' # 0.0.0
```

If the author would rather not hold an account-scoped PyPI token, PyPI also accepts
a "pending publisher" registered before the project exists, and the first CI release
then creates the project. npm has no equivalent, so the npm placeholder is always
needed.

## 5. Prompt the author to register the trusted publisher

Registry settings are not visible from any CLI, so stop here and hand the author the
exact values. Do not open the PR as ready, and do not merge, until they confirm both
are registered. The two registries check different OIDC claims, so the workflow
Comment on lines +173 to +175

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Delay npm publisher registration until release

This ordering requires registering the npm publisher before the PR is even ready, but npm expires a new trusted-publisher configuration unless its first successful publish occurs within two days. Any PR that spends more than two days in review will therefore reach its first release with an expired configuration and fail; instruct authors to register or recreate it within two days of the planned release instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trusted publisher expires before first release

High Severity

The skill registers the npm trusted publisher during the package PR, but unused npm trusted-publisher configs expire after 48 hours. The first OIDC publish is the later 0.1.0 CI release, so the binding lapses and that release fails after the version bump is already on main.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0fef604. Configure here.

filename differs:

- npm checks the **calling** workflow. `npm publish` runs inside
`publish_packages.yml`, which `release.yml` calls, and npm's docs say validation
"checks the calling workflow's name instead of the workflow that actually contains
the publish command". The provenance on every published `e2b` version names
`.github/workflows/release.yml`.
- PyPI checks `job_workflow_ref`, which names the **called** workflow, so it wants
`publish_packages.yml` (this is how `e2b`, `e2b-code-interpreter` and
`e2b-desktop` are registered, see PR #1828).
Comment on lines +183 to +185

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register the caller workflow with PyPI

For a release started by release.yml, PyPI matches the caller's workflow_ref; it does not support registering the reusable publish_packages.yml via job_workflow_ref. PyPI's current troubleshooting documentation explicitly says reusable workflows cannot be used as trusted publishers, so the values given here produce invalid-publisher when uv publish runs. Register release.yml, or move the upload into a non-reusable workflow.

Useful? React with 👍 / 👎.


Tell the author:

> `@e2b/<name>@0.0.0` and `e2b-<name>` `0.0.0` are on the registries. Before the
> first release, register a GitHub Actions trusted publisher on each:
>
> npm: npmjs.com → `@e2b/<name>` → Settings → Publishing access → Trusted publisher
> → GitHub Actions
>
> | Field | Value |
> | -------------------- | ------------- |
> | Organization or user | `e2b-dev` |
> | Repository | `E2B` |
> | Workflow filename | `release.yml` |
> | Environment name | leave empty |
Comment on lines +199 to +200

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enable direct npm publishing in the setup table

New trusted-publisher configurations now default to allowing staged publishing, while permission for direct npm publish must be selected separately, according to the npm trusted-publishing documentation. Because this table is presented as the exact configuration but omits the allowed-action field, a newly configured package can reject the release workflow's npm publish; explicitly tell the author to enable direct publishing.

Useful? React with 👍 / 👎.

>
> PyPI: pypi.org → `e2b-<name>` → Manage → Publishing → GitHub
>
> | Field | Value |
> | ---------------- | ---------------------- |
> | Owner | `e2b-dev` |
> | Repository name | `E2B` |
> | Workflow name | `publish_packages.yml` |
> | Environment name | leave empty |
>
> Leave the environment empty to match the existing packages. The publish job does
> run in the `deployment` environment, so entering `deployment` also works and is
> stricter; only do that if the other packages have been tightened the same way.

Neither registry validates these values when saved; a typo surfaces as a 404 or 403
at the first release. Case matters: the repository is `E2B`, the filenames include
`.yml`.

## 6. Open the PR

Base the branch on `main`. The PR description gets a "Required registry
configuration (before merging)" section with the two tables above and a checkbox
per registry, the way PR #1828 and PR #1850 did, so the state of the manual steps is
visible on the PR and not only in chat. Link the Linear issue.

After the first release, confirm it on both registries:

```sh
npm view @e2b/<name> versions
curl -s https://pypi.org/pypi/e2b-<name>/json | python3 -c 'import json,sys; print(sorted(json.load(sys.stdin)["releases"]))'
```

`0.0.0` stays listed next to `0.1.0`; that is expected and matches `@e2b/id`.
Loading