How to Release Utilities Package to GitHub Packages
Releasing a closed-source, reusable JavaScript/TypeScript package for internal use across frontend and backend is a common challenge, especially when you want to automate it, keep things modular, and avoid unnecessary leakage of code. Here’s how I do it, step by step, using only what’s needed for a stable, repeatable workflow. Why GitHub Packages (and Not npmjs.org)? Most teams reach for npmjs.org by default, but if your utilities are strictly internal - or have some private contract processing logic you’re not ready to open-source - GitHub’s own registry is more than enough: Integrated with your repository: No extra accounts or keys to manage. Scoped access: Control exactly who gets your code. Familiar workflows: Your team’s already on GitHub; why hop away? I've used this for smart contract SDKs referenced both by frontend app and NestJS API. Directory Structure I keep only my distributable code in /package, separate from internal scripts/docs, to avoid accidentally leaking dev files |-- .github/ |-- src/ |-- package/ #

Releasing a closed-source, reusable JavaScript/TypeScript package for internal use across frontend and backend is a common challenge, especially when you want to automate it, keep things modular, and avoid unnecessary leakage of code. Here’s how I do it, step by step, using only what’s needed for a stable, repeatable workflow.
Why GitHub Packages (and Not npmjs.org)?
Most teams reach for npmjs.org by default, but if your utilities are strictly internal - or have some private contract processing logic you’re not ready to open-source - GitHub’s own registry is more than enough:
- Integrated with your repository: No extra accounts or keys to manage.
- Scoped access: Control exactly who gets your code.
- Familiar workflows: Your team’s already on GitHub; why hop away?
I've used this for smart contract SDKs referenced both by frontend app and NestJS API.
Directory Structure
I keep only my distributable code in /package
, separate from internal scripts/docs, to avoid accidentally leaking dev files
|-- .github/
|-- src/
|-- package/ # <--- Only your published files live here
|-- package.json
|-- dist/
|-- index.js
|-- ...
Pro tip: npm publish
runs only in /package
, not at the repo root.
Manual Releases Triggered From GitHub Releases
Every package update is explicitly tagged as a release in GitHub's UI, which helps prevent accidental releases of incomplete work.
Action Workflow File
Below is the full workflow that gets the job done.
name: Publish package on GitHub Packages
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
registry-url: "https://npm.pkg.github.com"
scope: "@your-user-name"
always-auth: true
- name: Install dependencies
run: npm ci
- name: Build package
run: npm run package
- name: Install package dependencies
working-directory: ./package
run: npm ci
- name: Publish package
working-directory: ./package
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Place it in github/workflows/publish.yml
Key Parts
- Scopes, Not Monorepos: No workspaces, no publishing the entire repo.
-
No Source Leakage: Only files in
/package
are seen by consumers—no accidental pushes of TS, docs, or git history. - Manual Trigger: The process kicks off only when you create a GitHub Release, not on every push or PR.
Real-World Example
Let’s say you update a Smart Contracts ABI in /src
, then run your internal build (maybe via a simple "package"
script) to output to /package/dist
.
Only that transpiled, dependency-free version ships.
Your API team can safely pull it via:
npm install @user/package-name --registry=https://npm.pkg.github.com
From both backend and frontend, with no npmjs exposure.