Guide

OpenAPI to GitHub Packages

Publish a TypeScript SDK from your OpenAPI document to GitHub Packages, re-published automatically every time the schema changes.

Last reviewed: April 23, 2026

Teams that already live on GitHub tend to reach for GitHub Packages the first time they need to ship a private SDK. The reasons are reasonable: the identity model is the same one the rest of the org already uses, billing is already set up, and the package URL sits next to the repository it came from. For most internal-SDK use cases the choice is over before it starts — unless you actually try to wire it up.

What you learn wiring it up is that GitHub Packages has more moving parts than a normal npm publish. The scope of the package must match the organisation. The `publishConfig` in `package.json` must point at `https://npm.pkg.github.com`. The publishing side needs a token with `write:packages`; `GITHUB_TOKEN` works inside Actions but expires per job and can't be used from anywhere else. Consumers still need an `.npmrc` with their own token and the right `@scope:registry` pin, and Dependabot silently doesn't see updates unless you add a second config block for GitHub Packages.

SDK Factory treats GitHub Packages as a first-class registry target. The encrypted token, the correct `publishConfig`, the scope alignment, and the consumer-side copy-paste `.npmrc` are all handled in one form. The SDK rebuilds and republishes on every schema change without anyone touching an Action.

Why the manual path hurts

What you actually spend time on when you wire this by hand — and what eventually drifts.

.npmrc on both sides, forever

The publisher needs one `.npmrc` to authenticate pushes. Every consumer needs a different `.npmrc` to authenticate reads. Both drift. Somebody rotates a token, the publisher fixes theirs, three consumer repos break silently a day later.

GITHUB_TOKEN vs PAT has real consequences

`GITHUB_TOKEN` is the clean option inside Actions — but it can't publish from anything else, expires per job, and loses permissions when workflows reorganise. Classic PATs work everywhere but outlive people. Fine-grained PATs work but have quotas and surprise expiries. Each has its own failure mode and nobody picks correctly the first time.

Scopes must match the org

`@acme/api-client` can only be published to GitHub Packages if the package scope `@acme` matches the organisation that owns the token's repo. A mismatch is a cryptic 403. Getting this right at setup is fine; remembering it a year later when the org renames isn't.

Dependabot doesn't see private updates by default

Dependabot ignores GitHub Packages until you add a second config block per repo with a token reference. Teams add it once and forget — and then the private SDK becomes the one dependency that silently doesn't upgrade.

Before and after

The hand-rolled version versus the pasted-URL version — same end state, very different footprint.

Doing it by handhand-rolled GitHub Packages publish
# package.json must carry a publishConfig block
{
  "name": "@acme/api-client",
  "publishConfig": {
    "registry": "https://npm.pkg.github.com",
    "access": "restricted"
  }
}

# .github/workflows/publish.yml
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write        # mandatory, often forgotten
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: https://npm.pkg.github.com
          scope: '@acme'
      - run: npm ci && npm run build
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# + consumer-side .npmrc in every downstream repo
# + dependabot.yml second registry block per repo
# + token-rotation SOP across the org
With SDK FactorySDK Factory dashboard
Paste schema URL:  https://api.example.com/openapi.yaml

Registry:          [ GitHub Packages ▼ ]
Scope:             @acme
Package name:      @acme/api-client
Token (PAT):       ••••••••••••  (encrypted)

┌──────────────────────────┐
│   Create & auto-publish  │
└──────────────────────────┘

→ publishConfig emitted for you.
→ Consumer .npmrc + Dependabot block shown on the app page.
→ Token stored encrypted, rotatable from the dashboard.

What SDK Factory does instead

The same pain points, handled by the pipeline — not by whoever is on call this week.

Pick GitHub Packages in the dashboard — done

Registry target is a dropdown. The scope, token, and `publishConfig` are derived from the form and emitted in the published package automatically. No hand-written Action, no `publishConfig` fixed up in a PR.

Token storage built for rotation

Paste a fine-grained PAT once, encrypted AES256 at rest. Rotation is a form-submit — the next publish picks up the new token automatically. No secret refactoring across three workflows.

Correct scope contract, every time

Scope mismatches between package name and org are caught at setup, not at the first 403 in production. The `publishConfig` section in the published `package.json` is always right for GitHub Packages' rules.

Consumer-side `.npmrc` is copy-pasted, not hand-rolled

The dashboard surfaces the exact `.npmrc` block consumers need, with the scope pre-filled. Dependabot config block is surfaced next to it — readers get both in one place instead of two Stack Overflow searches.

How GitHub Packages scopes actually work

GitHub Packages uses the npm scope (the `@…` prefix) as a routing key. `@acme/api-client` is published under the `acme` organisation — and GitHub only lets you publish it if the token belongs to that organisation with `write:packages`. There is no way to publish `@acme/thing` from a personal account, even with admin access on the target org, unless the token explicitly carries the org's permissions.

This is usually fine, except when it quietly isn't. If your GitHub org renames, every `@old-name/…` package keeps existing under the old scope; newly published versions can only use the new name. If you're preparing an acquisition or a rebrand, that matters. SDK Factory lets you re-point the scope + package name without touching the schema URL, so a rename is one form submit.

Token types and their pitfalls

`GITHUB_TOKEN` is the default token inside GitHub Actions. It works for publish, the permissions are automatic, and it disappears at the end of the job. That's a feature for security and a problem for anything outside Actions — a CLI, a SaaS like ours, or a script on a server. You cannot use `GITHUB_TOKEN` with SDK Factory.

Classic PATs work everywhere and live until you revoke them. That flexibility is also the risk — they tend to outlive the developer who created them, accrete permissions over time, and rotate only when someone notices. We support them but recommend fine-grained PATs for anything long-lived.

Fine-grained PATs are our default recommendation. You can scope them to a single organisation and a single permission (`write:packages`), give them a fixed expiry, and rotate from the UI. Every failure mode (expired, over-quota, revoked) surfaces as a 401 at publish time, which lands in the dashboard as REQUIRE_ACTION — not silently in a CI log.

What consumers still need on their side

Consumers read from GitHub Packages the same way publishers write to it: a scoped `.npmrc` line plus an auth token. The SDK Factory app page shows the exact block to paste into the consumer repo's `.npmrc` file, with the scope pre-substituted.

Dependabot consumption is separate. By default Dependabot only watches the public npm registry; to pick up private SDK updates, consumers need a registry block in their `dependabot.yml` with a token reference. That config is also shown on the app page, ready to copy. It's still their repo — we don't touch it — but they don't have to figure out the shape on their own.

Who reaches for this

  • Internal SDK distribution where the org is already on GitHub and a second registry would only add surface area.
  • Early-access customer SDKs gated by GitHub organisation membership — the customer's access to the package follows their access to the org.
  • Monorepos that consume multiple internal services, each with its own SDK, all published under the same `@org` scope.
  • Private forks of public SDKs where the maintaining team wants a separate publish target while keeping the package name familiar.

FAQ

Do consumers need anything special to install the SDK?

Yes — a two-line `.npmrc` with the scope pointed at `npm.pkg.github.com` plus an auth token. The exact block to paste is shown on the SDK Factory app page. That's unavoidable; GitHub Packages is private by default.

Which token type do you recommend?

A fine-grained PAT, scoped to a single organisation with `write:packages` and a 90-day expiry. It's the narrowest permission surface, the failure mode is loud (401 on publish → REQUIRE_ACTION in the dashboard), and rotation is a form submit.

Can I publish to public npm and GitHub Packages at the same time?

Today each app publishes to a single registry. The common workaround is two apps pointed at the same schema URL — one publishing to public npm under a customer-facing name, one publishing to GitHub Packages under the internal name. Both rebuild on every schema change.

Does this work with Dependabot?

Yes, once consumers add a registry block to their `dependabot.yml` with a token reference. We show the exact block on the app page. Without it, Dependabot silently skips GitHub Packages updates — that's a GitHub default, not something we can change from the publisher side.

What about GitLab Package Registry or other Git-host registries?

Anything speaking the standard npm protocol (a URL + a token) works via the `custom` registry option. That covers GitLab Package Registry, JFrog Artifactory's npm repos, Sonatype Nexus, AWS CodeArtifact, Verdaccio, and Cloudsmith. GitHub Packages has enough quirks to deserve its own dropdown; the rest share one.

Try it with your actual schema.

One app on the Free tier, no card required. Paste your OpenAPI URL and see the generated GitHub Packages in minutes.