feat(release): add npm package and automated npm publish#4
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds npm package distribution support for the codetok CLI tool, enabling users to install via npm install -g codetok. The package downloads pre-built binaries from GitHub Releases during postinstall, supporting macOS, Linux, and Windows on x64 and arm64 architectures. The release workflow is extended to automatically publish the npm package after creating GitHub releases.
Changes:
- Added npm package scaffold with launcher script, postinstall downloader, and package configuration
- Extended the release workflow to automatically publish to npm after GitHub release creation
- Updated documentation (README.md and README_zh.md) with npm installation instructions and release automation notes
Reviewed changes
Copilot reviewed 7 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| npm/package.json | Defines npm package metadata, dependencies, and configuration |
| npm/bin/codetok.js | Node.js launcher script that spawns the downloaded native binary |
| npm/scripts/install.mjs | Postinstall script that downloads and verifies platform-specific binaries from GitHub Releases |
| npm/README.md | Documentation for the npm package installation and troubleshooting |
| npm/LICENSE | MIT license file for the npm package |
| npm/vendor/.gitkeep | Placeholder to ensure vendor directory exists in repository |
| .github/workflows/release.yml | Added publish-npm job to automate npm publishing after GitHub releases |
| README.md | Added npm installation section and release automation notes |
| README_zh.md | Chinese version of README.md updates |
| .gitignore | Updated to track npm launcher script while ignoring downloaded binaries and dependencies |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const vendorDir = path.join(pkgRoot, 'vendor'); | ||
|
|
||
| const project = 'codetok'; | ||
| const owner = 'Miss-you'; |
There was a problem hiding this comment.
The repository owner is inconsistent with the rest of the codebase. Throughout the project (go.mod, README.md, package.json), the repository is referenced as 'miss-you' (all lowercase), but here it's 'Miss-you' (with capital M and Y). While GitHub web URLs are case-insensitive, this inconsistency could cause issues with API calls or downloads. Change to 'miss-you' to match the established convention.
| const owner = 'Miss-you'; | |
| const owner = 'miss-you'; |
| async function downloadToFile(url, destPath, redirects = 0) { | ||
| if (redirects > maxRedirects) { | ||
| throw new Error(`too many redirects while downloading ${url}`); | ||
| } | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| const req = https.get( | ||
| url, | ||
| { | ||
| headers: { | ||
| 'User-Agent': 'codetok-npm-installer', | ||
| }, | ||
| }, | ||
| (res) => { | ||
| if ( | ||
| res.statusCode && | ||
| [301, 302, 303, 307, 308].includes(res.statusCode) && | ||
| res.headers.location | ||
| ) { | ||
| const nextURL = new URL(res.headers.location, url).toString(); | ||
| res.resume(); | ||
| downloadToFile(nextURL, destPath, redirects + 1).then(resolve).catch(reject); | ||
| return; | ||
| } | ||
|
|
||
| if (res.statusCode !== 200) { | ||
| res.resume(); | ||
| reject(new Error(`download failed (${res.statusCode}) for ${url}`)); | ||
| return; | ||
| } | ||
|
|
||
| const ws = fs.createWriteStream(destPath); | ||
| ws.on('error', reject); | ||
| ws.on('finish', () => resolve()); | ||
| res.pipe(ws); | ||
| } | ||
| ); | ||
|
|
||
| req.on('error', reject); | ||
| }); |
There was a problem hiding this comment.
The download function lacks timeout handling, which could cause npm installations to hang indefinitely if the download stalls. This is particularly problematic for CI/CD environments and automated installations. Consider adding a timeout (e.g., 30-60 seconds) to the HTTP request using setTimeout on the request object or implementing an AbortController-based timeout mechanism to ensure the installation fails cleanly rather than hanging.
| } | ||
|
|
||
| const ws = fs.createWriteStream(destPath); | ||
| ws.on('error', reject); |
There was a problem hiding this comment.
When the write stream encounters an error, the response stream should be destroyed to prevent memory leaks and ensure clean resource cleanup. Currently, if the write stream fails, the response continues piping data. Add 'res.destroy()' in the write stream error handler to properly clean up the response stream when write operations fail.
| ws.on('error', reject); | |
| ws.on('error', (err) => { | |
| res.destroy(); | |
| reject(err); | |
| }); |
| env: | ||
| TAG: ${{ github.ref_name }} | ||
| run: | | ||
| VERSION="${TAG#v}" |
There was a problem hiding this comment.
The version extraction from the git tag lacks validation. If the tag doesn't match the expected 'vX.Y.Z' format (e.g., if someone pushes 'v1.2' or 'v1.2.3-beta'), the extracted version could be invalid for npm's semantic versioning requirements. Consider adding validation to ensure the extracted version matches npm's semver format before setting it in package.json, or at minimum add a check that the VERSION variable is not empty and contains only valid semver characters.
| VERSION="${TAG#v}" | |
| VERSION="${TAG#v}" | |
| # Ensure VERSION is non-empty and matches a valid semantic version | |
| if [ -z "$VERSION" ]; then | |
| echo "Error: VERSION extracted from tag '$TAG' is empty." | |
| exit 1 | |
| fi | |
| # Accept versions like 1.2.3 or 1.2.3-beta.1 | |
| if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then | |
| echo "Error: VERSION '$VERSION' extracted from tag '$TAG' is not a valid semantic version (expected X.Y.Z or X.Y.Z-prerelease)." | |
| exit 1 | |
| fi |
|
|
||
| const actual = await sha256(filePath); | ||
| if (actual !== expected) { | ||
| throw new Error(`checksum mismatch for ${archiveName}`); |
There was a problem hiding this comment.
The checksum verification error message is not helpful for debugging. When the checksum mismatch occurs, users cannot determine whether the download was corrupted or if there's a mismatch in the release artifacts. Consider including both the expected and actual checksums in the error message (e.g., 'checksum mismatch for ${archiveName}: expected ${expected}, got ${actual}') to help users and maintainers diagnose the issue.
| throw new Error(`checksum mismatch for ${archiveName}`); | |
| throw new Error(`checksum mismatch for ${archiveName}: expected ${expected}, got ${actual}`); |
Summary
codetokso users can install vianpm install -g codetok.Changes
npm/:npm/package.jsonnpm/bin/codetok.jsnpm/scripts/install.mjsnpm/README.mdnpm/LICENSEpostinstallnow downloads the matching binary from GitHub Releases and verifies SHA256 viachecksums.txt..github/workflows/release.yml):publish-npmjob (runs after release job)vX.Y.Z->X.Y.Z) and publish to npmREADME.mdREADME_zh.md.gitignorefor npm package artifacts while preserving tracked launcher script.Validation
npm pack --dry-run(innpm/)make testmake lintmake vetmake buildNotes
NPM_TOKENfor npm publish job.v*tag will publish both GitHub Release artifacts and npm package.