Skip to content

fix(cli-generator): ship README, LICENSE and metadata in npm packages - #17551

Open
devin-ai-integration[bot] wants to merge 2 commits into
devin/1787579847-cli-generator-rust-setupfrom
devin/1787858001-npm-package-readme-license
Open

fix(cli-generator): ship README, LICENSE and metadata in npm packages#17551
devin-ai-integration[bot] wants to merge 2 commits into
devin/1787579847-cli-generator-rust-setupfrom
devin/1787858001-npm-package-readme-license

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Description

Linear ticket: Refs

npm renders a package's page from the README inside the published tarball, so a README sitting in the source repo is invisible to the registry. The generated publish/publish-launcher steps write package.json from scratch and copy only the binary (or bin/cli.js), so every generated CLI's npm page reads "no README data" — e.g. @elevenlabs/cli at 1.1.0. The launcher also carried no license field and a hardcoded "CLI for <binaryName>" description, ignoring the packageIdentity.description / license the consumer already set for the crate and the Homebrew formula.

Not retroactively fixable — the page is a property of a published version, so it lands with the consumer's next release.

Changes Made

  • emitPublishWorkflow stages docs into every package dir, in both the platform matrix and the launcher step:
    for doc in README.md LICENSE LICENSE.md LICENSE.txt; do
      if [[ -f "${doc}" ]]; then cp "${doc}" "${PKG_DIR}/${doc}"; fi
    done
    Existence is tested rather than assumed: a README is emitted for every generated CLI, but LICENSE only exists when the consumer configured github.license, and these steps run under set -euo pipefail. files is left alone — npm includes README/LICENSE in the tarball regardless.
  • New packageMetadata argument (description, license) threaded from runPipeline, defaulting off packageIdentity and defaultCrateDescription(...) — the same values the crate's [package] block and the Homebrew formula already use. license is omitted entirely when unset rather than emitted empty.

Testing

  • Unit tests added/updated — @fern-api/cli-generator: 425 passing. New cases assert the copy loop appears in both publish steps, that the configured description/license reach both package.json bodies, and that no "license" key is emitted when unset.
  • Manual testing completed — emitted ci.yml for an ElevenLabs-shaped OIDC config; parses as YAML, and every run: block passes bash -n (16 steps).

Stacked on #17507 (same file, and its --manifest-path test steps are in the same test job).

Link to Devin session: https://app.devin.ai/sessions/0c32af0182744cbe8d22f33a85a93160
Open in Devin Desktop: https://app.devin.ai/desktop/session/0c32af0182744cbe8d22f33a85a93160?variant=devin


Devin Review

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Review Summary

Copies README/LICENSE into each package dir before publish and threads description/license into the generated package.json bodies. The copy loop is correct and guarded for missing files. Main gap: the new description/license values are user-supplied config interpolated raw into an unquoted shell heredoc containing JSON, so quotes, newlines, $ or backticks will corrupt the generated package.json (or worse, get expanded by the shell).

  • 🟡 1 warning(s)
  • 🔵 1 suggestion(s)

To request another review, comment /ai-review on this pull request.

Comment on lines +235 to +241
const { binaryName, npmPublishInfo, repoUrl, packageMetadata } = args;
const launcherDescription = packageMetadata.description ?? `CLI for ${binaryName}`;
const licenseField =
packageMetadata.license != null
? `
"license": "${packageMetadata.license}",`
: "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

packageMetadata.description / license come from consumer config and are interpolated raw into an unquoted heredoc (<<PKGJSON, since ${VERSION} etc. expand) that emits JSON inside a YAML block scalar. A description containing " produces invalid JSON, a newline breaks the block indentation, and $(...)/backticks get executed by the publish job. Escape before emitting:

Suggested change
const { binaryName, npmPublishInfo, repoUrl, packageMetadata } = args;
const launcherDescription = packageMetadata.description ?? `CLI for ${binaryName}`;
const licenseField =
packageMetadata.license != null
? `
"license": "${packageMetadata.license}",`
: "";
const { binaryName, npmPublishInfo, repoUrl, packageMetadata } = args;
// Emitted into an unquoted heredoc: JSON-escape quotes/newlines, then
// backslash-escape `$`/backtick so the shell doesn't expand them (the
// backslashes are consumed by the heredoc, leaving valid JSON).
const escapeForHeredocJson = (value: string): string =>
JSON.stringify(value).slice(1, -1).replace(/[$`]/g, "\\$&");
const launcherDescription = escapeForHeredocJson(packageMetadata.description ?? `CLI for ${binaryName}`);
const licenseField =
packageMetadata.license != null
? `
"license": "${escapeForHeredocJson(packageMetadata.license)}",`
: "";

* `github.license`, and `set -e` would otherwise fail the publish on a
* missing file.
*/
const PACKAGE_DOCS_COPY_STEP = `

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

This fragment depends on the step's cwd being the checkout root (and on PKG_DIR being absolute or relative to that same cwd). In the platform step it lands just before cd "${PKG_DIR}", so that holds; worth double-checking the launcher step doesn't cd anywhere earlier, otherwise the copy silently no-ops and we're back to "no README data" without a failing publish.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

Devin Review

Comment thread generators/cli/src/emitPublishWorkflow.ts
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant