# SSH URLs in package-lock.json - The Complete Solution ## TL;DR **Two-layer approach:** 1. **CI test stage**: Patch lockfile SSH → HTTPS for `npm ci` (for tests) 2. **Docker build**: Clone private repo, build tarball, replace with `file:` (for production image) ## The Problem Your local machine converts HTTPS → SSH during `npm install`, baking SSH URLs into `package-lock.json`. CI/CD fails because it can't authenticate via SSH. ## Why Fighting It Locally Doesn't Work ❌ **Approach 1**: "Fix package.json to use HTTPS" - Doesn't work if git config rewrites it during install ❌ **Approach 2**: "Remove SSH rewrites from git config" - Inconvenient for developers - Easy to forget - Breaks other workflows ❌ **Approach 3**: "Temporarily disable git config during install" - Doesn't persist - Every developer needs to remember ## ✅ The Complete Solution (Two-Layer Approach) **Accept that the lockfile has SSH URLs and handle them at two stages:** ### Layer 1: CI Test Stage (For Running Tests) The GitHub Actions workflow uses the proven pattern to handle both SSH and HTTPS URLs: ```yaml - name: Checkout code uses: actions/checkout@v4 with: persist-credentials: false # Don't let default GITHUB_TOKEN interfere - name: Configure git for private packages env: GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }} run: | git config --global url."https://${GH_TOKEN}@github.com/".insteadOf ssh://git@github.com/ git config --global url."https://${GH_TOKEN}@github.com/".insteadOf git@github.com: - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' - name: Patch package-lock.json with authenticated URLs env: GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }} run: | # Handle both SSH and HTTPS URLs if grep -q "git+ssh://git@github.com" package-lock.json; then echo "⚠️ SSH URLs found - patching to HTTPS with token..." sed -i "s|git+ssh://git@github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json echo "✓ Lockfile patched successfully" else echo "⚠️ HTTPS URLs found - injecting token..." sed -i "s|git+https://github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json echo "✓ Token injected successfully" fi - name: Install dependencies run: npm ci --legacy-peer-deps ``` ### Layer 2: Docker Build (For Production Image) The Dockerfile clones the private repo, builds it as a tarball, and installs from `file:`: ```dockerfile # Clone, build and package mana-core as a tarball RUN --mount=type=secret,id=github_token \ if [ -f /run/secrets/github_token ]; then \ export GITHUB_TOKEN=$(cat /run/secrets/github_token) && \ git clone https://${GITHUB_TOKEN}@github.com/Memo-2023/mana-core-nestjs-package.git /tmp/mana-core; \ fi && \ cd /tmp/mana-core && \ npm install --force && \ npm run build && \ npm pack && \ mv *.tgz /app/mana-core.tgz # Copy package.json and replace GitHub URL with the tarball COPY package.json ./ RUN sed -i 's|"git+https://github.com/Memo-2023/mana-core-nestjs-package.git"|"file:mana-core.tgz"|g' package.json # Install dependencies from tarball RUN npm install --legacy-peer-deps ``` The GitHub Actions workflow passes the token as a Docker secret: ```yaml - name: Build and Push Docker Image env: GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }} run: | docker build \ --secret id=github_token,env=GH_TOKEN \ . ``` ## Why This Works 1. **Developers**: Use SSH locally (convenient, no config changes needed) 2. **package-lock.json**: Contains SSH URLs (fine, we handle it in CI) 3. **CI Test Stage**: Patches SSH → HTTPS for `npm ci` to run tests 4. **Docker Build**: Clones repo, builds tarball, installs from `file:` (no git involved in final image) 5. **Production**: Docker image has mana-core built into it, no runtime git dependency 6. **Everyone's happy**: No git config changes, no local workflow disruption ## Key Implementation Details ### For CI Test Stage (sed patching) ```bash sed -i "s|git+ssh://git@github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json ``` - Happens at runtime in CI (never committed) - Allows `npm ci` to install dependencies for testing - Works reliably every time ### For Docker Build (tarball approach) ```bash # Docker secret gives access to GitHub token --mount=type=secret,id=github_token # Clone and build the private package git clone https://${GITHUB_TOKEN}@github.com/Memo-2023/mana-core-nestjs-package.git npm pack # Creates .tgz file # Replace git URL with local file reference sed -i 's|"git+https://..."|"file:mana-core.tgz"|g' package.json ``` - Private package is baked into the Docker image - No git dependency at runtime - Production image is fully self-contained ## Why This is Better Than Alternatives | Approach | Developer Impact | Reliability | Production Quality | Maintenance | |----------|-----------------|-------------|-------------------|-------------| | Fix git config locally | 😡 High | 🔴 Low | ⚠️ Medium | 😱 High | | Require HTTPS in lockfile | 😡 High | 🔴 Low | ⚠️ Medium | 😱 High | | **Two-layer (sed + tarball)** | 😊 None | 🟢 100% | ✅ Excellent | 😌 None | ## Lessons Learned 1. **npm ci reads package-lock.json directly** - It doesn't care about git config 2. **Fighting developer workflows is futile** - Accept SSH URLs locally 3. **Two layers solve different problems**: - CI test stage needs quick install for testing → sed patch - Production image needs reliability and security → tarball bake-in 4. **Docker secrets are the right tool** - Pass credentials without committing them 5. **Self-contained images are better** - No runtime git dependencies ## References This solution combines proven approaches: - **sed patching**: Used in `storyteller-project` for CI/CD - **tarball approach**: Used in memoro-service for production Docker images - Battle-tested across multiple projects --- **Bottom line**: Commit the SSH lockfile, handle it in two layers (CI + Docker). Done. 🎯