Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ on:
- cron: '15 16 * * 2'

env:
NPM_CONFIG_REGISTRY: https://packagefeedproxy.microsoft.io/npm/
PIP_INDEX_URL: https://packagefeedproxy.microsoft.io/pypi/simple/
NPM_CONFIG_REGISTRY: https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/Pylance_PublicPackages/npm/registry/
PIP_INDEX_URL: https://pkgs.dev.azure.com/devdiv/DevDiv/_packaging/debugpy/pypi/simple/

jobs:
analyze:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
permissions: {}

env:
PIP_INDEX_URL: https://packagefeedproxy.microsoft.io/pypi/simple/
PIP_INDEX_URL: https://pkgs.dev.azure.com/devdiv/DevDiv/_packaging/debugpy/pypi/simple/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue · Please address or respond

📍 .github/workflows/pr-check.yml:9
High: The Skeptic reproduced HTTP 401 responses and failed pip resolution against this index, but this workflow and push-check.yml:14 provide no feed authentication before installing Python dependencies on fresh GitHub-hosted runners. Configure supported authentication before the first installation, and retain an accessible feed path for fork PRs where credentials cannot be supplied.

[verified]

NODE_VERSION: 22.17.0
TEST_RESULTS_DIRECTORY: .
# Force a path with spaces and unicode chars to test extension works in these scenarios
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/push-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
permissions: {}

env:
PIP_INDEX_URL: https://packagefeedproxy.microsoft.io/pypi/simple/
PIP_INDEX_URL: https://pkgs.dev.azure.com/devdiv/DevDiv/_packaging/debugpy/pypi/simple/
NODE_VERSION: 22.17.0
TEST_RESULTS_DIRECTORY: .
# Force a path with spaces and unicode chars to test extension works in these scenarios
Expand Down
5 changes: 3 additions & 2 deletions build/azure-devdiv-pipeline.pre-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ variables:
- name: VsixName
value: python-debugger.vsix
- name: NPM_CONFIG_REGISTRY
value: 'https://packagefeedproxy.microsoft.io/npm/'
value: 'https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/Pylance_PublicPackages/npm/registry/'
- name: PIP_PROXY_INDEX_URL
value: 'https://packagefeedproxy.microsoft.io/pypi/simple/'
# PipAuthenticate supplies the internal index URL with credentials before buildSteps.
value: '$(PIP_INDEX_URL)'

parameters:
- name: publishExtension
Expand Down
5 changes: 3 additions & 2 deletions build/azure-devdiv-pipeline.stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ variables:
- name: VsixName
value: python-debugger.vsix
- name: NPM_CONFIG_REGISTRY
value: 'https://packagefeedproxy.microsoft.io/npm/'
value: 'https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/Pylance_PublicPackages/npm/registry/'
- name: PIP_PROXY_INDEX_URL
value: 'https://packagefeedproxy.microsoft.io/pypi/simple/'
# PipAuthenticate supplies the internal index URL with credentials before buildSteps.
value: '$(PIP_INDEX_URL)'

parameters:
- name: publishExtension
Expand Down
4 changes: 2 additions & 2 deletions build/package-feeds.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"pip": "https://pypi.org/simple/"
},
"microsoft": {
"npm": "https://packagefeedproxy.microsoft.io/npm/",
"pip": "https://packagefeedproxy.microsoft.io/pypi/simple/"
"npm": "https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/Pylance_PublicPackages/npm/registry/",
"pip": "https://pkgs.dev.azure.com/devdiv/DevDiv/_packaging/debugpy/pypi/simple/"
}
}
27 changes: 17 additions & 10 deletions build/setup-npm-registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ const { readFileSync, writeFileSync } = require('node:fs');
const path = require('node:path');
const { microsoft } = require('./package-feeds.json');

function rewritePackageLock(source) {
function normalizeRegistry(registry) {
return registry.endsWith('/') ? registry : `${registry}/`;
}

function rewritePackageLock(source, registry = microsoft.npm) {
const lockfile = JSON.parse(source);
if (
![2, 3].includes(lockfile.lockfileVersion) ||
Expand All @@ -17,18 +21,19 @@ function rewritePackageLock(source) {
throw new Error('Expected an npm lockfile with lockfileVersion 2 or 3 and a packages section.');
}

const normalizedRegistry = normalizeRegistry(registry);
return source.replace(/("resolved"\s*:\s*)("(?:\\.|[^"\\])*")/g, (match, prefix, encodedUrl) => {
const url = JSON.parse(encodedUrl);
const rewritten = url.replace(/^https:\/\/registry\.(?:npmjs\.org|yarnpkg\.com)\//, microsoft.npm);
const rewritten = url.replace(/^https:\/\/registry\.(?:npmjs\.org|yarnpkg\.com)\//, normalizedRegistry);
return rewritten === url ? match : prefix + JSON.stringify(rewritten);
});
}

function rewritePackageLocks(root = path.resolve(__dirname, '..')) {
function rewritePackageLocks(root = path.resolve(__dirname, '..'), registry = microsoft.npm) {
const files = [path.join(root, 'package-lock.json')];
const updates = files.map((file) => {
const source = readFileSync(file, 'utf8');
return { file, source, rewritten: rewritePackageLock(source) };
return { file, source, rewritten: rewritePackageLock(source, registry) };
});
for (const { file, source, rewritten } of updates) {
if (rewritten !== source) {
Expand All @@ -48,13 +53,15 @@ if (require.main === module) {
if (install && !process.env.npm_execpath) {
throw new Error('Run npm install using npm run ci:msft.');
}
rewritePackageLocks();
const registry = normalizeRegistry(
process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY || microsoft.npm,
);
rewritePackageLocks(undefined, registry);
if (install) {
const result = spawnSync(
process.execPath,
[process.env.npm_execpath, 'ci', `--registry=${microsoft.npm}`],
{ cwd: path.resolve(__dirname, '..'), stdio: 'inherit' },
);
const result = spawnSync(process.execPath, [process.env.npm_execpath, 'ci', `--registry=${registry}`], {
cwd: path.resolve(__dirname, '..'),
stdio: 'inherit',
});
if (result.error || result.signal || result.status !== 0) {
const reason = result.error?.message || result.signal || `exit code ${result.status}`;
const error = new Error(`npm ci failed: ${reason}`);
Expand Down
52 changes: 43 additions & 9 deletions build/setup-npm-registry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const { rewritePackageLock, rewritePackageLocks } = require('./setup-npm-registr
const { normalizePackageLock } = require('./normalize-package-lock');

const publicRegistry = 'https://registry.npmjs.org/';
const proxyRegistry = 'https://packagefeedproxy.microsoft.io/npm/';
const microsoftRegistry = 'https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/Pylance_PublicPackages/npm/registry/';
const source = `${JSON.stringify(
{
lockfileVersion: 3,
Expand All @@ -34,20 +34,27 @@ const source = `${JSON.stringify(

test('rewrites public resolved URLs without changing versions, integrity or formatting', () => {
const expected = source
.replaceAll(publicRegistry, proxyRegistry)
.replace('https://registry.yarnpkg.com/', proxyRegistry);
.replaceAll(publicRegistry, microsoftRegistry)
.replace('https://registry.yarnpkg.com/', microsoftRegistry);
assert.equal(rewritePackageLock(source), expected);
assert.equal(rewritePackageLock(expected), expected);
assert.equal(normalizePackageLock(expected), source.replace('https://registry.yarnpkg.com/', publicRegistry));
});

test('normalizes slashless registry overrides before rewriting resolved URLs', () => {
for (const registry of [publicRegistry, microsoftRegistry]) {
const expected = source.replaceAll(publicRegistry, registry).replace('https://registry.yarnpkg.com/', registry);
assert.equal(rewritePackageLock(source, registry.slice(0, -1)), expected);
}
});

test('supports version 2 legacy dependencies as well as packages', () => {
const legacy = JSON.stringify({
lockfileVersion: 2,
packages: {},
dependencies: { example: { resolved: `${publicRegistry}example/-/example.tgz` } },
});
assert.equal(rewritePackageLock(legacy), legacy.replace(publicRegistry, proxyRegistry));
assert.equal(rewritePackageLock(legacy), legacy.replace(publicRegistry, microsoftRegistry));
});

test('preserves other fields, private feeds, Git dependencies and lookalike hosts', () => {
Expand Down Expand Up @@ -100,20 +107,20 @@ test('missing or invalid root lockfile fails explicitly', (t) => {
assert.throws(() => rewritePackageLocks(root), /Expected an npm lockfile/);
});

function runCli({ args = ['--ci'], npmCli = 'npm-cli.js', result = { status: 0 } } = {}) {
function runCli({ args = ['--ci'], npmCli = 'npm-cli.js', result = { status: 0 }, env = {} } = {}) {
const calls = [];
const errors = [];
const process = {
argv: ['node', 'setup-npm-registry.js', ...args],
env: { npm_execpath: npmCli },
env: { npm_execpath: npmCli, ...env },
execPath: 'node',
};
const module = { exports: {} };
const mockedRequire = (name) => {
if (name === 'node:fs') {
return {
readFileSync: () => source,
writeFileSync: (file) => calls.push({ file }),
writeFileSync: (file, source) => calls.push({ file, source }),
};
}
if (name === 'node:child_process') {
Expand All @@ -137,19 +144,46 @@ function runCli({ args = ['--ci'], npmCli = 'npm-cli.js', result = { status: 0 }
return { calls, errors, process };
}

test('ci:msft rewrites the root lockfile before installing with the shared proxy registry', () => {
test('ci:msft rewrites the root lockfile before installing with the Microsoft registry', () => {
const context = runCli();
assert.equal(context.calls.length, 2);
assert.ok(context.calls[0].file.endsWith('package-lock.json'));
assert.equal(context.calls[1].command, 'node');
assert.deepEqual(context.calls[1].args, ['npm-cli.js', 'ci', `--registry=${proxyRegistry}`]);
assert.deepEqual(context.calls[1].args, ['npm-cli.js', 'ci', `--registry=${microsoftRegistry}`]);
assert.equal(context.calls[1].cwd, path.resolve(__dirname, '..'));
assert.equal(context.calls[1].stdio, 'inherit');
assert.equal(context.errors.length, 0);
const scripts = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).scripts;
assert.equal(scripts['ci:msft'], 'node ./build/setup-npm-registry.js --ci');
});

test('ci:msft preserves template authentication for the configured internal registry', () => {
const registry = 'https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/Pylance_PublicPackages/npm/registry/';
for (const settings of [
{ NPM_CONFIG_REGISTRY: registry },
{ npm_config_registry: registry },
{ NPM_CONFIG_REGISTRY: registry.slice(0, -1) },
{ npm_config_registry: registry.slice(0, -1) },
{ NPM_CONFIG_REGISTRY: 'https://stale.example/npm/', npm_config_registry: registry },
]) {
const env = {
...settings,
NPM_CONFIG_USERCONFIG: path.join(tmpdir(), 'authenticated.npmrc'),
NODE_AUTH_TOKEN: 'test-token',
};
const context = runCli({ env });
const install = context.calls.at(-1);
assert.deepEqual(install.args, ['npm-cli.js', 'ci', `--registry=${registry}`]);
assert.equal(install.env, undefined);
assert.equal(context.process.env.NPM_CONFIG_USERCONFIG, env.NPM_CONFIG_USERCONFIG);
assert.equal(context.process.env.NODE_AUTH_TOKEN, env.NODE_AUTH_TOKEN);
for (const write of context.calls.slice(0, -1)) {
assert.equal(write.source, rewritePackageLock(source, registry));
}
assert.equal(context.errors.length, 0);
}
});

for (const result of [{ status: 7 }, { status: null, signal: 'SIGTERM' }, { error: new Error('not found') }]) {
test(`ci:msft reports npm failure: ${result.signal || result.error?.message || result.status}`, () => {
const context = runCli({ result });
Expand Down
14 changes: 10 additions & 4 deletions build/setup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,15 @@ for (const platform of ['win32', 'linux']) {
for (const call of calls) {
assert.equal(
call.env.npm_config_registry,
microsoft ? 'https://packagefeedproxy.microsoft.io/npm/' : 'https://registry.npmjs.org/',
microsoft
? 'https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/Pylance_PublicPackages/npm/registry/'
: 'https://registry.npmjs.org/',
);
assert.equal(
call.env.PIP_INDEX_URL,
microsoft ? 'https://packagefeedproxy.microsoft.io/pypi/simple/' : 'https://pypi.org/simple/',
microsoft
? 'https://pkgs.dev.azure.com/devdiv/DevDiv/_packaging/debugpy/pypi/simple/'
: 'https://pypi.org/simple/',
);
assert.equal(call.env.NPM_CONFIG_REGISTRY, undefined);
assert.equal(call.env.HTTPS_PROXY, context.originalEnv.HTTPS_PROXY);
Expand Down Expand Up @@ -180,16 +184,18 @@ test('Microsoft entry point selects the shared Microsoft setup implementation',

test('PR and push installs use the npm helper without a redundant registry environment setting', () => {
const root = path.join(__dirname, '..');
const { microsoft } = require('./package-feeds.json');
for (const file of ['pr-check.yml', 'push-check.yml']) {
const workflow = readFileSync(path.join(root, '.github', 'workflows', file), 'utf8');
assert.doesNotMatch(workflow, /^\s*NPM_CONFIG_REGISTRY:/m);
assert.match(workflow, /^\s*PIP_INDEX_URL: https:\/\/packagefeedproxy\.microsoft\.io\/pypi\/simple\//m);
assert.ok(workflow.split(/\r?\n/).includes(` PIP_INDEX_URL: ${microsoft.pip}`));
assert.doesNotMatch(workflow, /^\s*run: npm ci\b/m);
}
for (const action of ['lint', 'build-vsix']) {
const workflow = readFileSync(path.join(root, '.github', 'actions', action, 'action.yml'), 'utf8');
assert.match(workflow, /^\s*run: npm run ci:msft\s*$/m);
}
const codeql = readFileSync(path.join(root, '.github', 'workflows', 'codeql-analysis.yml'), 'utf8');
assert.match(codeql, /^\s*NPM_CONFIG_REGISTRY: https:\/\/packagefeedproxy\.microsoft\.io\/npm\//m);
assert.ok(codeql.split(/\r?\n/).includes(` NPM_CONFIG_REGISTRY: ${microsoft.npm}`));
assert.ok(codeql.split(/\r?\n/).includes(` PIP_INDEX_URL: ${microsoft.pip}`));
});
Loading