Skip to content

Conversation

@jeremywiebe
Copy link
Contributor

@jeremywiebe jeremywiebe commented Nov 19, 2025

Summary:

This PR introduces a new tool to help with publishing a new package. We (Khan Academy) have moved to Trusted Publishing for all npm package publishing but one can't configure Trusted Publishing until the package exists on npmjs.com (do you see a problem here? ;)

To help with this, this tool guides the author through publishing a placeholder package. Once the placeholder has been published, we can configure it and from then on publish via Trusted Publishing.

Issue: LEMS-3681

Test plan:

I tested this by running pnpm build in the repo on this branch.
Then I switched to /tmp and ran the following commands (note that it failed to publish because I gave it a bogus token). I'm hesitant to use a real token as I don't want to pollute npmjs with dummy packages.

$ cd /tmp/placeholder-package-demo/
$ echo "{
       \"name\": \"placeholder-package-tool-demo\"
     }" > package.json
$ pnpm add file:///Users/jeremy/khan/wonder-stuff/packages/wonder-stuff-tool-new-pkg/
Packages: +1
+
Progress: resolved 1, reused 1, downloaded 0, added 1, done

dependencies:
+ @khanacademy/wonder-stuff-tool-publish-new-pkg 0.0.1

Done in 298ms using pnpm v10.8.0
$ git init 
Initialized empty Git repository in /private/tmp/placeholder-package-demo/.git/
$ git remote add origin git@github.com:Khan/wonder-stuff.git
$ pnpm exec wonder-stuff-tool-publish-new-pkg @khanacademy/hot-new-package
Validating package name: @khanacademy/hot-new-package
✓ Package name is valid

Detecting git repository...
✓ Detected repository: Khan/wonder-stuff

Creating placeholder package...
✓ Created temporary directory: /var/folders/wt/k3j496f16qj317tdw22lr5gh0000gn/T/npm-placeholder-zeDOnh
✓ Created placeholder files in /var/folders/wt/k3j496f16qj317tdw22lr5gh0000gn/T/npm-placeholder-zeDOnh

=== npm Granular Access Token Required ===

A granular access token is needed to publish the placeholder package.

When creating the token, please configure it with:
  ✓ Expiration: 7 days (the default, or less if you prefer)
  ✓ Ensure 'Bypass two-factor authentication' is checked
  ✓ Permissions:
    • Read and write - only for the '@khanacademy' scope

Opening a browser window so that you can create the token...

After creating the token, copy it and paste it below (input will be hidden):

Access Token: npm_dummytokenthatImadeup1234567

✓ Token format is valid
✓ Configured npm authentication for temporary directory

=== Publishing Package ===

Publishing from /var/folders/wt/k3j496f16qj317tdw22lr5gh0000gn/T/npm-placeholder-zeDOnh...

npm notice
npm notice 📦  @khanacademy/hot-new-package@0.0.1
npm notice Tarball Contents
npm notice 258B README.md
npm notice 36B index.js
npm notice 457B package.json
npm notice Tarball Details
npm notice name: @khanacademy/hot-new-package
npm notice version: 0.0.1
npm notice filename: khanacademy-hot-new-package-0.0.1.tgz
npm notice package size: 549 B
npm notice unpacked size: 751 B
npm notice shasum: 11e3306cdf072e268dfd2e1d87107d6bd264f82e
npm notice integrity: sha512-w9YPIA/Q1vd8T[...]rJG2cKLvZr5EA==
npm notice total files: 3
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access
npm error code E404
npm error 404 Not Found - PUT https://registry.npmjs.org/@khanacademy%2fhot-new-package - Not found
npm error 404
npm error 404  '@khanacademy/hot-new-package@0.0.1' is not in this registry.
npm error 404
npm error 404 Note that you can also install from a
npm error 404 tarball, folder, http url, or git url.
npm error A complete log of this run can be found in: /Users/jeremy/.npm/_logs/2025-11-24T21_50_33_379Z-debug-0.log

✗ Error: pnpm publish failed
✓ Cleaned up temporary directory: /var/folders/wt/k3j496f16qj317tdw22lr5gh0000gn/T/npm-placeholder-zeDOnh
$ _

@changeset-bot
Copy link

changeset-bot bot commented Nov 19, 2025

⚠️ No Changeset found

Latest commit: ba9f4a3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

jeremywiebe and others added 2 commits November 19, 2025 12:23
…ing or encoding

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Copy link
Member

@somewhatabstract somewhatabstract left a comment

Choose a reason for hiding this comment

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

This looks pretty good to me as a concept. I really dislike the AI generated code; lots of things not following our best practices. I've left some comments.

Main things;

  • the unit tests should follow our best practices and conventions
  • I really hate files that have tons of functions in them. It makes unnecessary decision points; where do I put my new function? Which file does it fit with? And it makes code discovery and workspace navigation harder, with cryptic filenames that don't necessarily describe their contents well, with files that end up being really huge and hard to navigate. It's so much easier to just have one file per function. Generally makes testing easier too and helps to make sure things that need tests have tests.


describe("git", () => {
beforeEach(() => {
jest.clearAllMocks();
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Use resetAllMocks so that mocked implementations are also reset. This keeps the mocks around, but strips them of any configured returns or implementations.

Copy link
Contributor Author

@jeremywiebe jeremywiebe Nov 22, 2025

Choose a reason for hiding this comment

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

Turns out I don't need them at all because we reset them in our base Jest config. I removed this and all tests continue to pass.

@jeremywiebe
Copy link
Contributor Author

@somewhatabstract I think I've addressed all your comments (except the verbose logging suggestion). Tagging you for one more review. Feel free to tag anyone else you think would be interested.

@jeremywiebe jeremywiebe marked this pull request as ready for review November 24, 2025 21:55
@khan-actions-bot khan-actions-bot requested a review from a team November 24, 2025 21:55
@khan-actions-bot
Copy link
Contributor

Gerald

Required Reviewers
  • @Khan/frontend-infra for changes to .eslintrc.js, pnpm-lock.yaml, packages/wonder-stuff-tool-new-pkg/README.md, packages/wonder-stuff-tool-new-pkg/package.json, packages/wonder-stuff-tool-new-pkg/tsconfig-build.json, packages/wonder-stuff-tool-new-pkg/src/fs.ts, packages/wonder-stuff-tool-new-pkg/src/generate-index-js.ts, packages/wonder-stuff-tool-new-pkg/src/generate-package-json.ts, packages/wonder-stuff-tool-new-pkg/src/generate-readme.md.ts, packages/wonder-stuff-tool-new-pkg/src/git.ts, packages/wonder-stuff-tool-new-pkg/src/index.ts, packages/wonder-stuff-tool-new-pkg/src/npm.ts, packages/wonder-stuff-tool-new-pkg/src/open-browser.ts, packages/wonder-stuff-tool-new-pkg/src/parse-args.ts, packages/wonder-stuff-tool-new-pkg/src/print-next-steps.ts, packages/wonder-stuff-tool-new-pkg/src/publish-placeholder-pkg.ts, packages/wonder-stuff-tool-new-pkg/src/types.ts, packages/wonder-stuff-tool-new-pkg/src/write-package-files.ts, packages/wonder-stuff-tool-new-pkg/src/__tests__/fs.test.ts, packages/wonder-stuff-tool-new-pkg/src/__tests__/generate-package-json.test.ts, packages/wonder-stuff-tool-new-pkg/src/__tests__/git.test.ts, packages/wonder-stuff-tool-new-pkg/src/__tests__/write-package-files.test.ts, packages/wonder-stuff-tool-new-pkg/src/bin/wonder-stuff-tool-new-pkg.ts

Don't want to be involved in this pull request? Comment #removeme and we won't notify you of further changes.

@somewhatabstract
Copy link
Member

Looks like you have some lint errors

Comment on lines +17 to +26
const tempDir = fs.mkdtempSync(
path.join(os.tmpdir(), `test-git-repo-${crypto.randomUUID()}`),
);

execSync("git init", {cwd: tempDir});
execSync(`git remote add origin "${remoteUrl}"`, {cwd: tempDir});

tempDirs.push(tempDir);

return tempDir;
Copy link
Member

@somewhatabstract somewhatabstract Dec 1, 2025

Choose a reason for hiding this comment

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

suggestion: This feels a little scary; making actual file system changes, but it's in temp so that's good.

I think that it perhaps needs to be a little more defensive. What happens if execSync throws?

Suggested change
const tempDir = fs.mkdtempSync(
path.join(os.tmpdir(), `test-git-repo-${crypto.randomUUID()}`),
);
execSync("git init", {cwd: tempDir});
execSync(`git remote add origin "${remoteUrl}"`, {cwd: tempDir});
tempDirs.push(tempDir);
return tempDir;
const tempDir = fs.mkdtempSync(
path.join(os.tmpdir(), `test-git-repo-${crypto.randomUUID()}`),
);
tempDirs.push(tempDir);
execSync("git init", {cwd: tempDir});
execSync(`git remote add origin "${remoteUrl}"`, {cwd: tempDir});
return tempDir;

Comment on lines +30 to +31
const dirsToDelete = tempDirs.splice(0, tempDirs.length);
dirsToDelete.forEach((dir) => fs.rmSync(dir, {recursive: true}));
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: I think that this needs to be more defensize. I would also use a for...of loop rather than the forEach, but that's a stylistic nit.

Suggested change
const dirsToDelete = tempDirs.splice(0, tempDirs.length);
dirsToDelete.forEach((dir) => fs.rmSync(dir, {recursive: true}));
const dirsToDelete = tempDirs.splice(0, tempDirs.length);
for (const dir of dirsToDelete) {
try {
fs.rmSync(dir, {recursive: true});
} catch {
/* ignore */
}
}

Copy link
Member

@somewhatabstract somewhatabstract left a comment

Choose a reason for hiding this comment

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

Mostly looks good. Some housekeeping and related things:

  • I don't think this package should export anything. It should just have a the bin and that's it.
  • The tool implementation should just be in the bin file, it doesn't need to be elsewhere
  • The function docs aren't following our conventions

);

// Act
const act = () => detectGitRepoOriginUrl(nonExistantWorkingDir);
Copy link
Member

Choose a reason for hiding this comment

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

nit: In general, since React testing uses act for a specific thing in its domain, we tend to use underTest for this across our frontend code. Not a big deal, though.

Comment on lines +14 to +15
const dirsToDelete = tempDirs.splice(0, tempDirs.length);
dirsToDelete.forEach((dir) => fs.rmSync(dir, {recursive: true}));
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: I think this needs to be more defensive.

Suggested change
const dirsToDelete = tempDirs.splice(0, tempDirs.length);
dirsToDelete.forEach((dir) => fs.rmSync(dir, {recursive: true}));
const dirsToDelete = tempDirs.splice(0, tempDirs.length);
for (const dir of dirsToDelete) {
try {
fs.rmSync(dir, {recursive: true});
} catch {
/* ignore */
}
}

});

async function makeTempDirAndWritePackageFiles() {
// Arrange
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Unnecessary // Arrange.

Suggested change
// Arrange

const localName = crypto.randomUUID();
const packageName = `@khan/${localName}`;

// Act
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Unnecessary // Act

Suggested change
// Act

Comment on lines +20 to +22
const tempDir = fs.mkdtempSync(
path.join(os.tmpdir(), "write-package-files-tests-"),
);
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: This is not getting added to the tempDirs array for cleanup.

Copy link
Member

Choose a reason for hiding this comment

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

suggestion: It's unnecessary to have the code live in a separate file here, I think. Just have it all in this file.

/**
* The parsed arguments from the command line.
*/
interface ParsedArgs {
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Missing export?

*
* If invalid/incompatible parameters were provided, prints usage info.
*/
export function parseArgs(): ParsedArgs {
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Not importing the type

Comment on lines +8 to +9
* Generates and writes the placeholder package files using tempDir as the
* package working directory.
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Our convention is to use the imperative in a single line first, and then a broader description. Also, should document params and return.

Suggested change
* Generates and writes the placeholder package files using tempDir as the
* package working directory.
* Generate and write the placeholder package files.
*
* This generated and writes the placeholder package files using
* the given dir as the package working directory.
*
* @param tempDir The package working directory.
* @param packageName The package name.
* @param repoName The repo name to use for the package.json.
* @returns A promise that resolves when the package files are written.

Comment on lines +56 to +57
// Allow console.log in tool packages
"packages/wonder-stuff-tool-*/src/**/*.ts",
Copy link
Member

Choose a reason for hiding this comment

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

praise: Nice.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants