BBC's guide to development
  • General

    • About
    • Tools
    • Git(hub)
    • Showpad
    • Hosting
    • Maintenance
    • Security
    • Go live checklist
  • Front-end development

    • Bundlers
    • CSS/SCSS
    • Javascript
    • Vue
    • PHP
    • Mails
    • Dev Faq
  • Functions
  • Mixins
  • General

    • OOP Structure
  • Component Classes

    • Accordion
    • App
    • Component
    • HighwayApp
    • Popup
    • PNG Sequencer
    • Tab
  • Manager Classes

    • BountListenerMgr
    • Cache
    • Configuration
    • InViewStateMgr
    • Instance Manager
    • Event dispatcher
  • Factories

    • SwiperFactory
  • PDF

    • AssetLoader
    • BasePdfDoc
    • TemplatePdfDoc
    • CustomPdfDoc
  • Utility functions

    • canvas
    • Connection Status
    • css
    • dev
    • placeholder
    • dom
    • fetch
    • json
    • object
    • scroll
    • scrollbar
    • spreadsheets
    • string
    • url
  • General

    • ComponentMgr
    • ThreeJsViewer
  • Components

    • ComponentMgr
    • GltfModel
    • Snappable
    • Socket
    • ThreeJsViewer
    • ThreeJsViewerCamera
  • Loaders

    • ConfigurationSerializer
    • GltfBlockParser
  • Utils

    • CanvasInputAdapter
    • CollisionManager
    • SocketGridExpander
    • blender
    • headless
  • General

    • Troubleshooting
    • Legacy
  • Components

    • AssetBar
    • ConfigGenerator
    • ShowpadApp
  • Managers

    • Assets
    • AppsDb
    • Config
  • Utils

    • Connection Status
    • general
    • showpad-interactive
    • showpad-upload
  • Components

    • Accordion
    • BackButton
    • Breadcrumb
    • ByltButton
    • Hamburger
    • Icon
    • Logo
    • Loader
    • Modal
    • Popup
    • Prompt
    • ProgressBar
    • TextLoader
  • Composables

    • useDebugMode
    • useConnectionStatus
  • Utils

    • dom
    • props
  • General

    • General
    • Tracking
  • Components

    • Accordion
    • ActionButton
    • AssetItem
    • AssetList
    • BackButton
    • ConfigGenButton
    • Logo
    • Media
    • Modal
    • Popup
    • Prompt
    • SPButton
    • SPRouterView
    • SPTrackedRouterLink
    • TextLoader
    • View
  • Composables

    • useConnectionStatus
  • Stores

    • useAppsDbStore
    • useBreadcrumbStore
    • useShowpadAPIStore
    • useShowpadSDKStore
    • useSpConfigStore
    • useSpStore
    • useSpTrackingStore
  • The New Kit

    • General
    • Installation & Usage
    • ACF Blocks
    • PHPCS
    • Functions
    • Vite
    • WP Config
    • Staging Deployment
  • Best Practices

    • Page Structure
    • Fonts/Typography
  • Todo
GitHub
  • General

    • About
    • Tools
    • Git(hub)
    • Showpad
    • Hosting
    • Maintenance
    • Security
    • Go live checklist
  • Front-end development

    • Bundlers
    • CSS/SCSS
    • Javascript
    • Vue
    • PHP
    • Mails
    • Dev Faq
  • Functions
  • Mixins
  • General

    • OOP Structure
  • Component Classes

    • Accordion
    • App
    • Component
    • HighwayApp
    • Popup
    • PNG Sequencer
    • Tab
  • Manager Classes

    • BountListenerMgr
    • Cache
    • Configuration
    • InViewStateMgr
    • Instance Manager
    • Event dispatcher
  • Factories

    • SwiperFactory
  • PDF

    • AssetLoader
    • BasePdfDoc
    • TemplatePdfDoc
    • CustomPdfDoc
  • Utility functions

    • canvas
    • Connection Status
    • css
    • dev
    • placeholder
    • dom
    • fetch
    • json
    • object
    • scroll
    • scrollbar
    • spreadsheets
    • string
    • url
  • General

    • ComponentMgr
    • ThreeJsViewer
  • Components

    • ComponentMgr
    • GltfModel
    • Snappable
    • Socket
    • ThreeJsViewer
    • ThreeJsViewerCamera
  • Loaders

    • ConfigurationSerializer
    • GltfBlockParser
  • Utils

    • CanvasInputAdapter
    • CollisionManager
    • SocketGridExpander
    • blender
    • headless
  • General

    • Troubleshooting
    • Legacy
  • Components

    • AssetBar
    • ConfigGenerator
    • ShowpadApp
  • Managers

    • Assets
    • AppsDb
    • Config
  • Utils

    • Connection Status
    • general
    • showpad-interactive
    • showpad-upload
  • Components

    • Accordion
    • BackButton
    • Breadcrumb
    • ByltButton
    • Hamburger
    • Icon
    • Logo
    • Loader
    • Modal
    • Popup
    • Prompt
    • ProgressBar
    • TextLoader
  • Composables

    • useDebugMode
    • useConnectionStatus
  • Utils

    • dom
    • props
  • General

    • General
    • Tracking
  • Components

    • Accordion
    • ActionButton
    • AssetItem
    • AssetList
    • BackButton
    • ConfigGenButton
    • Logo
    • Media
    • Modal
    • Popup
    • Prompt
    • SPButton
    • SPRouterView
    • SPTrackedRouterLink
    • TextLoader
    • View
  • Composables

    • useConnectionStatus
  • Stores

    • useAppsDbStore
    • useBreadcrumbStore
    • useShowpadAPIStore
    • useShowpadSDKStore
    • useSpConfigStore
    • useSpStore
    • useSpTrackingStore
  • The New Kit

    • General
    • Installation & Usage
    • ACF Blocks
    • PHPCS
    • Functions
    • Vite
    • WP Config
    • Staging Deployment
  • Best Practices

    • Page Structure
    • Fonts/Typography
  • Todo
GitHub
  • GitHub Actions: Private npm Dependencies over SSH

GitHub Actions: Private npm Dependencies over SSH

This document explains how to configure a GitHub Action so npm ci can install private npm dependencies that are referenced through Git SSH URLs.

Example dependency URL:

ssh://git@github.com/BBCnv/bbcdev.front-end-kit.git

Typical failure when SSH access is broken:

npm ERR! code 128
npm ERR! An unknown git error occurred
npm ERR! command git --no-replace-objects ls-remote ssh://git@github.com/BBCnv/bbcdev.front-end-kit.git
npm ERR! ERROR: Repository not found.
npm ERR! fatal: Could not read from remote repository.

Mental model

There are two repositories involved:

Consuming repository
  Runs the GitHub Action
  Stores the private SSH key in an Actions secret

Private dependency repository
  Contains the package npm needs to install
  Stores the matching public SSH key as a deploy key

For example:

Consuming repository
  secrets.BYLT_ACTIONS_SSH_KEY
        |
        | private key loaded by webfactory/ssh-agent
        v
npm ci
        |
        | git ls-remote ssh://git@github.com/BBCnv/bbcdev.front-end-kit.git
        v
Private dependency repository
  BBCnv/bbcdev.front-end-kit
  Public key added under Settings → Deploy keys

GitHub Actions does not automatically get access to every private repository in the same organization. If one private repo depends on another private repo, the workflow still needs credentials that can read the dependency repo.

Recommended setup

Use a dedicated read-only deploy key for the private dependency repository.

This is better than relying on a personal user SSH key because:

  • it is explicit;
  • it is tied to the repository dependency;
  • it does not depend on a human user staying in the organization;
  • it is easy to rotate;
  • it is read-only by default.

First-time setup

1. Generate a dedicated SSH key pair

Run this locally:

ssh-keygen -t ed25519 -C "GitHub Actions access to private npm dependency" -f ./BYLT_ACTIONS_SSH_KEY -N ""

This creates two files:

BYLT_ACTIONS_SSH_KEY       ← private key
BYLT_ACTIONS_SSH_KEY.pub   ← public key

Do not commit either file.

2. Add the public key to the private dependency repo

Go to the private repo that npm needs to fetch.

Example:

BBCnv/bbcdev.front-end-kit

Then open:

Settings → Deploy keys → Add deploy key

Use a clear title, for example:

BYLT Actions private npm access

Paste the contents of the public key:

cat ./BYLT_ACTIONS_SSH_KEY.pub

Leave this unchecked:

Allow write access

For npm install access, read-only access is enough.

3. Add the private key to the consuming repo secret

Go to the repository where the GitHub Action runs:

Settings → Secrets and variables → Actions

Create or update this secret:

BYLT_ACTIONS_SSH_KEY

Paste the full private key:

cat ./BYLT_ACTIONS_SSH_KEY

The value must include the full block:

-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----

Do not paste the .pub file here.

4. Load the SSH key before npm ci

In the GitHub Actions workflow, load the key before installing dependencies:

- name: Setup SSH key for private repo access
  uses: webfactory/ssh-agent@v0.9.0
  with:
    ssh-private-key: ${{ secrets.BYLT_ACTIONS_SSH_KEY }}

- name: Install dependencies
  run: npm ci

The SSH setup step must run before npm ci, because npm invokes Git internally when it encounters SSH-based package dependencies.

Recommended workflow snippet

This is a practical version with an explicit access test before npm ci.

- name: Checkout repository
  uses: actions/checkout@v4

- name: Setup SSH key for private repo access
  uses: webfactory/ssh-agent@v0.9.0
  with:
    ssh-private-key: ${{ secrets.BYLT_ACTIONS_SSH_KEY }}

- name: Test SSH access to GitHub private dependencies
  run: |
    echo "Testing GitHub SSH authentication..."
    ssh -T git@github.com || true

    echo ""
    echo "Testing access to private npm dependency..."
    git ls-remote ssh://git@github.com/BBCnv/bbcdev.front-end-kit.git

- name: Read Node version from .nvmrc
  id: nvmrc
  run: echo "node_version=$(cat .nvmrc | tr -d 'v')" >> $GITHUB_OUTPUT

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: ${{ steps.nvmrc.outputs.node_version }}
    cache: npm

- name: Install dependencies
  run: npm ci

The diagnostic step is optional, but useful. It makes private repo access failures obvious before npm wraps the error.

Minimal workflow snippet

If you do not want the diagnostic step permanently:

- name: Checkout repository
  uses: actions/checkout@v4

- name: Setup SSH key for private repo access
  uses: webfactory/ssh-agent@v0.9.0
  with:
    ssh-private-key: ${{ secrets.BYLT_ACTIONS_SSH_KEY }}

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: 20.19.5
    cache: npm

- name: Install dependencies
  run: npm ci

Recovery procedure when it breaks

Use this when the workflow suddenly starts failing at npm ci.

1. Identify the failing dependency

Look for this line in the GitHub Actions logs:

npm ERR! command git --no-replace-objects ls-remote ssh://git@github.com/ORG/REPO.git

Example:

npm ERR! command git --no-replace-objects ls-remote ssh://git@github.com/BBCnv/bbcdev.front-end-kit.git

This tells you which private repo npm cannot access.

2. Check the Actions secret

In the consuming repo:

Settings → Secrets and variables → Actions

Confirm this secret exists:

BYLT_ACTIONS_SSH_KEY

You cannot view the current value, so if in doubt, rotate it.

3. Check the deploy key on the private dependency repo

In the private dependency repo:

Settings → Deploy keys

Confirm the matching public key exists.

If there is no deploy key, create a new key pair and add the public key there.

4. Rotate the key if needed

Generate a new key:

ssh-keygen -t ed25519 -C "GitHub Actions access to private npm dependency" -f ./BYLT_ACTIONS_SSH_KEY -N ""

Add:

BYLT_ACTIONS_SSH_KEY.pub

to:

Private dependency repo → Settings → Deploy keys

Add:

BYLT_ACTIONS_SSH_KEY

to:

Consuming repo → Settings → Secrets and variables → Actions → BYLT_ACTIONS_SSH_KEY

Then rerun the workflow.

Common errors and what they mean

ERROR: Repository not found

Example:

npm ERR! command git --no-replace-objects ls-remote ssh://git@github.com/BBCnv/bbcdev.front-end-kit.git
npm ERR! ERROR: Repository not found.
npm ERR! fatal: Could not read from remote repository.

Usually means one of these:

  • the SSH key does not have access to the private repo;
  • the deploy key was removed;
  • the private key secret was replaced with the wrong key;
  • the repo was renamed, moved, or deleted;
  • the dependency URL has a typo;
  • the key belongs to a GitHub user who no longer has access.

How to detect:

- name: Test SSH access to private dependency
  run: |
    ssh -T git@github.com || true
    git ls-remote ssh://git@github.com/BBCnv/bbcdev.front-end-kit.git

How to fix:

  • confirm the repo URL is correct;
  • confirm the public key is added to the dependency repo as a deploy key;
  • confirm the private key is stored in the consuming repo as BYLT_ACTIONS_SSH_KEY;
  • rotate the key if unsure.

Permission denied (publickey)

Example:

git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Usually means GitHub did not accept the SSH key at all.

Possible causes:

  • the secret contains the wrong key;
  • the secret contains the public key instead of the private key;
  • the private key was pasted incompletely;
  • the matching public key is not registered with GitHub;
  • the SSH agent step did not run before npm ci.

How to fix:

  • verify the webfactory/ssh-agent step is before npm ci;
  • rotate the key;
  • ensure the private key is in the Actions secret;
  • ensure the public key is in the dependency repo deploy keys.

Host key verification failed

Example:

Host key verification failed.
fatal: Could not read from remote repository.

Usually means the runner does not trust GitHub’s SSH host key.

This is uncommon with normal GitHub Actions setups, but can happen with custom SSH configuration.

Possible fix:

- name: Add GitHub to known hosts
  run: |
    mkdir -p ~/.ssh
    ssh-keyscan github.com >> ~/.ssh/known_hosts

Place this before npm ci.

npm WARN EBADENGINE Unsupported engine

Example:

npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'vite@7.1.12',
npm WARN EBADENGINE   required: { node: '^20.19.0 || >=22.12.0' },
npm WARN EBADENGINE   current: { node: 'v20.12.2', npm: '10.5.0' }
npm WARN EBADENGINE }

This is separate from SSH access.

It means the Node.js version used by the workflow is older than what one or more packages require.

If the workflow reads from .nvmrc, update .nvmrc.

Example:

20.19.5

Then commit the change.

If the workflow hardcodes Node:

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: 20.19.5
    cache: npm

How to tell if the SSH key is valid but lacks repo access

Add this temporary step:

- name: Test SSH access to GitHub private dependencies
  run: |
    echo "Testing GitHub SSH authentication..."
    ssh -T git@github.com || true

    echo ""
    echo "Testing access to private npm dependency..."
    git ls-remote ssh://git@github.com/BBCnv/bbcdev.front-end-kit.git

Possible result:

Hi some-user! You've successfully authenticated, but GitHub does not provide shell access.
ERROR: Repository not found.
fatal: Could not read from remote repository.

This means:

The SSH key is valid, but the GitHub user/key does not have access to the private repo.

Best fix:

Use a dedicated deploy key instead of a user-owned SSH key.

Notes about deploy keys

Deploy keys are SSH keys attached directly to a GitHub repository.

For this use case:

public key  → private dependency repo → Deploy keys
private key → consuming repo → Actions secret

Deploy keys are read-only by default.

Use read-only deploy keys for npm installs.

Do not enable write access unless the workflow needs to push to the dependency repo.

Notes about same-organization repositories

Being in the same GitHub organization is not enough.

A workflow running in one private repo does not automatically get read access to another private repo in the same organization.

You still need one of the following:

  • a deploy key on the private dependency repo;
  • a GitHub App token with access to the dependency repo;
  • a fine-grained personal access token with access to the dependency repo;
  • a machine user with SSH access to the dependency repo.

For this setup, the deploy key approach is usually the simplest.

Checklist for future incidents

When npm ci fails with Git SSH errors:

[ ] Find the failing repo in the npm error line. [ ] Confirm the dependency URL is correct. [ ] Confirm the consuming repo has BYLT_ACTIONS_SSH_KEY. [ ] Confirm the private dependency repo has a deploy key. [ ] Confirm the workflow loads webfactory/ssh-agent before npm ci. [ ] Add temporary git ls-remote diagnostic if unclear. [ ] Rotate the key if the current setup cannot be verified. [ ] Update .nvmrc if EBADENGINE warnings appear.

Example final working setup

name: Deploy to Production

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup SSH key for private repo access
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.BYLT_ACTIONS_SSH_KEY }}

      - name: Test SSH access to private npm dependency
        run: |
          ssh -T git@github.com || true
          git ls-remote ssh://git@github.com/BBCnv/bbcdev.front-end-kit.git

      - name: Read Node version from .nvmrc
        id: nvmrc
        run: echo "node_version=$(cat .nvmrc | tr -d 'v')" >> $GITHUB_OUTPUT

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ steps.nvmrc.outputs.node_version }}
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build for production
        run: npm run build

After the setup has proven stable, the diagnostic step can be removed or kept permanently.

Edit this page
Last Updated: 4/27/26, 12:56 PM
Contributors: Nicolas Jaenen