Conversation
hosted-git-info's default GitLab tarball URL routes through `/api/v4/projects/<user>%2F<project>/...`. The `%2F` survives into the virtual store directory name (depPathToFilename only escapes raw `/`, not `%`), and Node refuses to import any module whose path contains an encoded slash. The same URL is also intermittently rejected by GitLab with a 406. Override the GitLab tarballtemplate to the `/-/archive/` URL, which works for both public and private repos and contains no encoded slashes. Closes #11533
📝 WalkthroughWalkthroughThis PR changes GitLab tarball URL generation to use the repository archive subdirectory format (/‑/archive//...tar.gz), adds a gitlabTarballTemplate helper and integrates it into fromHostedGit, updates tests to validate the new URL form, and adds a cspell allowlist entry for the new field name. ChangesGitLab Tarball Archive URL Fix
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
This PR fixes GitLab-hosted dependency installs that failed due to %2F (encoded slash) in the GitLab API tarball URL, which both triggered intermittent 406 Not Acceptable responses from GitLab and could produce virtual-store paths that Node rejects (ERR_INVALID_MODULE_SPECIFIER). It does so by overriding the GitLab tarball URL template to use GitLab’s /-/archive/ URL form, and updates relevant tests/fixtures accordingly.
Changes:
- Override
hosted-git-info’s GitLabtarballtemplateto generatehttps://<domain>/<user>/<project>/-/archive/<sha>/<project>-<sha>.tar.gzURLs. - Add a regression test for #11533 asserting the new GitLab tarball URL shape (no
%2F). - Update
pick-fetcherGitLab fixture URL and addtarballtemplatetocspell.json; add a changeset for patch releases.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| resolving/git-resolver/src/parseBareSpecifier.ts | Overrides GitLab tarball URL generation to avoid encoded slashes and GitLab API tarball endpoint. |
| resolving/git-resolver/test/index.ts | Adds/updates GitLab tarball URL assertions, including a regression test for #11533. |
| fetching/pick-fetcher/test/pickFetcher.ts | Updates the GitLab tarball URL fixture used to assert git-hosted tarball fetcher selection. |
| cspell.json | Adds tarballtemplate to the dictionary to avoid spellcheck noise. |
| .changeset/gitlab-tarball-archive-url.md | Documents the fix and triggers patch releases for affected packages. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Restore the skipped tests' original API-URL assertions; they document the old expected shape and weren't running anyway. Add the new `/-/archive/` URL to the pick-fetcher fixture as an additional case so both shapes are exercised.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
resolving/git-resolver/test/index.ts (1)
376-392: ⚡ Quick winCover both GitLab specifier forms in this regression test.
This test currently exercises only the HTTPS GitLab specifier. Since the bug report includes both
https://gitlab.com/...andgitlab:..., adding the shorthand form here would close a small coverage gap and guard parser-specific regressions.Suggested test refinement
-test('resolveFromGit() gitlab tarball uses /-/archive/ URL without encoded slash', async () => { +test.each([ + 'https://gitlab.com/pnpmjs/git-resolver', + 'gitlab:pnpmjs/git-resolver', +])('resolveFromGit() gitlab tarball uses /-/archive/ URL without encoded slash (%s)', async (bareSpecifier) => { const headCommit = '988c61e11dc8d9ca0b5580cb15291951812549dc' jest.mocked(fetchWithDispatcher).mockImplementation(async (_url, _opts) => { return { ok: true } as any // eslint-disable-line `@typescript-eslint/no-explicit-any` }) jest.mocked(git).mockImplementation(async () => ({ stdout: `${headCommit}\tHEAD` })) - const resolveResult = await resolveFromGit({ bareSpecifier: 'https://gitlab.com/pnpmjs/git-resolver' }) + const resolveResult = await resolveFromGit({ bareSpecifier }) expect(resolveResult).toStrictEqual({ id: `https://gitlab.com/pnpmjs/git-resolver/-/archive/${headCommit}/git-resolver-${headCommit}.tar.gz`, normalizedBareSpecifier: 'gitlab:pnpmjs/git-resolver', resolution: { tarball: `https://gitlab.com/pnpmjs/git-resolver/-/archive/${headCommit}/git-resolver-${headCommit}.tar.gz`, gitHosted: true, }, resolvedVia: 'git-repository', }) })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@resolving/git-resolver/test/index.ts` around lines 376 - 392, Add coverage for the GitLab shorthand form by duplicating or parameterizing the existing test for resolveFromGit() (test name: "resolveFromGit() gitlab tarball uses /-/archive/ URL without encoded slash") to also call resolveFromGit with bareSpecifier: 'gitlab:pnpmjs/git-resolver'; keep the same mocked git (git -> stdout `${headCommit}\tHEAD`) and fetchWithDispatcher mock, and assert the expected result object uses id and resolution.tarball `https://gitlab.com/pnpmjs/git-resolver/-/archive/${headCommit}/git-resolver-${headCommit}.tar.gz` and normalizedBareSpecifier 'gitlab:pnpmjs/git-resolver' so both the HTTPS and shorthand forms are validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@resolving/git-resolver/test/index.ts`:
- Around line 376-392: Add coverage for the GitLab shorthand form by duplicating
or parameterizing the existing test for resolveFromGit() (test name:
"resolveFromGit() gitlab tarball uses /-/archive/ URL without encoded slash") to
also call resolveFromGit with bareSpecifier: 'gitlab:pnpmjs/git-resolver'; keep
the same mocked git (git -> stdout `${headCommit}\tHEAD`) and
fetchWithDispatcher mock, and assert the expected result object uses id and
resolution.tarball
`https://gitlab.com/pnpmjs/git-resolver/-/archive/${headCommit}/git-resolver-${headCommit}.tar.gz`
and normalizedBareSpecifier 'gitlab:pnpmjs/git-resolver' so both the HTTPS and
shorthand forms are validated.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 52616c3a-4437-4423-842f-15d5bda9c6ce
📒 Files selected for processing (2)
fetching/pick-fetcher/test/pickFetcher.tsresolving/git-resolver/test/index.ts
✅ Files skipped from review due to trivial changes (1)
- fetching/pick-fetcher/test/pickFetcher.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: ubuntu-latest / Node.js 24 / Test
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,ts,tsx,jsx}: Use trailing commas in code following Standard Style with modifications
Prefer functions over classes
Declare functions after they are used, relying on hoisting
Functions should have no more than two or three arguments; use a single options object for functions needing more parameters
Organize imports in order: standard libraries, external dependencies (sorted alphabetically), then relative imports
Files:
resolving/git-resolver/test/index.ts
* fix(git-resolver): avoid encoded slash in GitLab tarball URL hosted-git-info's default GitLab tarball URL routes through `/api/v4/projects/<user>%2F<project>/...`. The `%2F` survives into the virtual store directory name (depPathToFilename only escapes raw `/`, not `%`), and Node refuses to import any module whose path contains an encoded slash. The same URL is also intermittently rejected by GitLab with a 406. Override the GitLab tarballtemplate to the `/-/archive/` URL, which works for both public and private repos and contains no encoded slashes. Closes #11533 * test: avoid cspell-flagged words * test: keep existing gitlab assertions, only add new ones Restore the skipped tests' original API-URL assertions; they document the old expected shape and weren't running anyway. Add the new `/-/archive/` URL to the pick-fetcher fixture as an additional case so both shapes are exercised.
Summary
Fixes #11533. Installation of dependencies referenced as
https://gitlab.com/<user>/<project>(orgitlab:<user>/<project>) was failing in two ways:ERR_PNPM_FETCH_406from GitLab when fetching the tarball.ERR_INVALID_MODULE_SPECIFIERbecause the virtual store directory contained an encoded slash.Both stem from the same root cause.
@pnpm/hosted-git-infobuilds GitLab tarball URLs ashttps://gitlab.com/api/v4/projects/<user>%2F<project>/repository/archive.tar.gz?sha=<commit>. The%2Fsurvives into the virtual-store directory name (depPathToFilenameonly escapes raw/, not%), and Node refuses any module path with an encoded slash. The same API URL is also intermittently rejected by GitLab with 406.This change overrides the
tarballtemplateforhosted.type === 'gitlab'to the standardhttps://gitlab.com/<user>/<project>/-/archive/<sha>/<project>-<sha>.tar.gzURL, which works for public and private repos and contains no encoded slashes. The URL still matches the existingisGitHostedPkgUrlcheck (https://gitlab.com/+tar.gz), so fetcher selection is unaffected.Test plan
resolving/git-resolver/test/index.tsthat resolves agitlab:specifier and asserts the new URL shape.pick-fetcherURL fixture.git-resolverandpick-fetchertest suites pass locally.Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
Bug Fixes
Tests
Chores