From 37b2de9b8b9d9b76156ba97d6a607bcf8ef91aa2 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Thu, 12 Nov 2026 18:29:42 +0100 Subject: [PATCH] Add the new-package skill: placeholder publish and OIDC trusted publisher Adding a package here has a manual tail that only fails at the first release: the name must exist on npm or PyPI before a trusted publisher can be attached, and the trusted publisher must name a different workflow file per registry. npm validates the calling workflow (release.yml), PyPI validates job_workflow_ref, which is the called workflow (publish_packages.yml). @e2b/id and e2b-id went through this by hand; the skill writes the procedure down so the next package does not rediscover it. The skill scaffolds from the ID packages, lists every CI and release file that names packages explicitly, publishes a 0.0.0 placeholder from a temp directory, and then stops to hand the author the exact trusted publisher values, since registry settings cannot be read or written from any CLI. The PR description carries the same tables so the manual step is visible on the PR. Co-Authored-By: Claude Fable 5.1 --- .claude/skills/new-package/SKILL.md | 233 ++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 .claude/skills/new-package/SKILL.md diff --git a/.claude/skills/new-package/SKILL.md b/.claude/skills/new-package/SKILL.md new file mode 100644 index 0000000000..b30d023c8f --- /dev/null +++ b/.claude/skills/new-package/SKILL.md @@ -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/` | `@e2b/` | `packages/js-` | +| PyPI | `e2b-` | `@e2b/python-` (private) | `packages/python-` | + +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_`. + +Check both names are free before anything else; a taken name means a different name, +not a different owner: + +```sh +npm view @e2b/ version # must fail with E404 +curl -sI https://pypi.org/pypi/e2b-/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-`): `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-`): `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/': minor +--- + +Initial release: . +``` + +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/_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. +- `.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/", + "version": "0.0.0", + "description": "Placeholder for . The first real release follows shortly.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/e2b-dev/e2b", + "directory": "packages/js-" + }, + "publishConfig": { "access": "public" } +} +EOF +printf '# @e2b/\n\nPlaceholder. The first real release follows shortly.\n' > README.md +npm whoami +npm publish --access public +npm view @e2b/ 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_ +printf '"""Placeholder for . The first real release follows shortly."""\n' > e2b_/__init__.py +cat > pyproject.toml <<'EOF' +[project] +name = "e2b-" +version = "0.0.0" +description = "Placeholder for . 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_"] +module-root = "" +EOF +uv build --wheel +# The author exports UV_PUBLISH_TOKEN in their own shell beforehand. +uv publish dist/*.whl +curl -s https://pypi.org/pypi/e2b-/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 +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). + +Tell the author: + +> `@e2b/@0.0.0` and `e2b-` `0.0.0` are on the registries. Before the +> first release, register a GitHub Actions trusted publisher on each: +> +> npm: npmjs.com → `@e2b/` → Settings → Publishing access → Trusted publisher +> → GitHub Actions +> +> | Field | Value | +> | -------------------- | ------------- | +> | Organization or user | `e2b-dev` | +> | Repository | `E2B` | +> | Workflow filename | `release.yml` | +> | Environment name | leave empty | +> +> PyPI: pypi.org → `e2b-` → 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/ versions +curl -s https://pypi.org/pypi/e2b-/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`.