managarten/manadeck/backend/SSH_LOCKFILE_SOLUTION.md
Till-JS e7f5f942f3 chore: initial commit - consolidate 4 projects into monorepo
Projects included:
- maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing)
- manacore (Expo mobile + SvelteKit web + Astro landing)
- manadeck (NestJS backend + Expo mobile + SvelteKit web)
- memoro (Expo mobile + SvelteKit web + Astro landing)

This commit preserves the current state before monorepo restructuring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:38:24 +01:00

176 lines
6 KiB
Markdown

# 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. 🎯