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
1 change: 1 addition & 0 deletions .githooks/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pre-commit text eol=lf
5 changes: 5 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

exec node build/normalize-package-lock.js --staged
6 changes: 4 additions & 2 deletions .github/actions/build-vsix/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ runs:
run: python -m pip install nox
shell: bash

- name: Run npm ci
run: npm ci --prefer-offline
- name: Install Node dependencies through Microsoft feeds
run: npm run ci:msft
env:
npm_config_prefer_offline: 'true'
shell: bash

- name: Install bundled python libraries
Expand Down
6 changes: 5 additions & 1 deletion .github/actions/lint/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ runs:
node-version: ${{ inputs.node_version }}
cache: 'npm'

- name: Test setup and hook scripts (no dependencies required)
run: npm run test:setup
shell: bash

- name: Install Node dependencies
run: npm ci
run: npm run ci:msft
shell: bash

- name: Lint TypeScript code
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ on:
schedule:
- cron: '15 16 * * 2'

env:
NPM_CONFIG_REGISTRY: https://packagefeedproxy.microsoft.io/npm/
PIP_INDEX_URL: https://packagefeedproxy.microsoft.io/pypi/simple/

jobs:
analyze:
if: github.repository_owner == 'microsoft'
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:

env:
PIP_INDEX_URL: https://packagefeedproxy.microsoft.io/pypi/simple/
NODE_VERSION: 24.17.0
PYTHON_VERSION: '3.10'
TEST_RESULTS_DIRECTORY: .
Expand Down Expand Up @@ -122,7 +123,7 @@ jobs:
cache-dependency-path: ${{ env.special-working-directory-relative }}/package-lock.json

- name: Install dependencies
run: npm ci
run: npm run ci:msft
shell: bash

- name: Compile TS tests
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/push-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
- 'release-*'

env:
PIP_INDEX_URL: https://packagefeedproxy.microsoft.io/pypi/simple/
NODE_VERSION: 24.17.0
PYTHON_VERSION: '3.10'
TEST_RESULTS_DIRECTORY: .
Expand Down Expand Up @@ -131,7 +132,7 @@ jobs:
cache-dependency-path: ${{ env.special-working-directory-relative }}/package-lock.json

- name: Install Node dependencies
run: npm ci
run: npm run ci:msft
shell: bash

- name: Compile TS tests
Expand Down
78 changes: 76 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,82 @@ If you already cloned without `--recurse-submodules`, initialize (or update) the
git submodule update --init --recursive
```

Then install dependencies:
Then choose the appropriate local package-feed setup below.

### Local package feeds

Use Node.js supported by this repository and Python 3.10 or later. Both setup commands create or reuse `.venv` and install the Python bundling tools. Choose the appropriate setup below before installing dependencies. Committed lockfiles use public npm URLs; Microsoft setup rewrites working copies for proxy installs.

#### Public contributors (non-Microsoft employees)

Use the public setup, which selects `https://registry.npmjs.org/` and `https://pypi.org/simple/` for its child processes. No Microsoft account or internal-feed credentials are needed, and this command works before `node_modules` exists:

From the repository root:

```bash
npm install
npm run setup
```

For subsequent npm installs, use `npm ci`. Public contributors do not need the Microsoft feed commands or lockfile hook.

#### Microsoft employees only

> **Microsoft users only:** The commands in this section configure Microsoft package feeds. Public contributors should use the public setup above.

Use the Microsoft setup, which selects `https://packagefeedproxy.microsoft.io/npm/` and `https://packagefeedproxy.microsoft.io/pypi/simple/` for its child processes:

```bash
npm run setup:msft
```

Microsoft setup initializes submodules, then rewrites public npm and Yarn `resolved` URLs in the root and shared TypeScript `package-lock.json` files to the npm proxy before running `npm ci`. Versions, integrity hashes, formatting, and private-feed URLs are preserved. The public setup leaves lockfiles unchanged.

After pulling a new lockfile from GitHub, rerun `npm run setup:msft`, or run the dependency-free rewrite step alone before your next install/build:

```bash
npm run setup:msft-registry
```

The standalone step requires the submodule to be initialized and both lockfiles to exist. Rewriting is idempotent and stops on missing or invalid lockfiles. Keep committed lockfile URLs public using the optional pre-commit hook below (or `npm run normalize-lockfile` for the root lockfile). The shared lockfile belongs to the submodule; do not commit its local proxy URL changes.

For subsequent root npm installs, use:

```bash
npm run ci:msft
```

This rewrites both lockfiles, then runs root `npm ci` with the proxy registry explicitly selected for package, audit, and metadata requests. It requires the submodule to be initialized; use `npm run setup:msft` for full setup, including the shared package and Python dependencies.

Local setup, lockfile rewriting, and `ci:msft` share the feed URLs in `build/package-feeds.json`.

Both setup commands use the same implementation, stop on errors, and keep registry overrides scoped to setup's child processes; they do not change your shell environment, edit `.npmrc`, or add credentials. The standalone rewrite step only edits lockfiles; it does not configure the registry for subsequent commands.

After setup, run `npm run compile` to build the extension. For subsequent Python commands, use `.venv\Scripts\python.exe` on Windows or `.venv/bin/python` on macOS/Linux (or activate that virtual environment). Registry overrides are not retained after setup.

#### Microsoft contributor pre-commit hook

If you use the Microsoft feeds and will commit changes, enable the recommended lockfile hook once per clone (no installed npm dependencies are required). It is not needed just to install or build:

```bash
npm run setup-hooks
```

The installer sets the clone's local `core.hooksPath` to `.githooks`, which also affects its linked worktrees. It refuses to replace an existing hook or a different hooks configuration. If you already use hooks, add `node build/normalize-package-lock.js --staged` to your existing pre-commit hook instead. Only enable this configuration in worktrees/branches that contain these hook files.

Before a local commit, the hook converts known `Pylance_PublicPackages`, `packagefeedproxy.microsoft.io/npm/`, and named `1es-public/npm-public` mirror tarball URLs in the staged root `package-lock.json` to public npm URLs. It preserves versions, integrity hashes, formatting, and intentional Azure-hosted dependencies such as `msft_consumption`. Unknown feeds are left unchanged. It re-stages only the lockfile; if normalization is needed and the lockfile has unstaged changes, the commit is blocked without modifying it. Stage or stash those changes, then retry.

To normalize manually, run `npm run normalize-lockfile`, review the changes, and stage them. To disable the installed hook, run `git config --local --unset core.hooksPath`. These are local Git hooks: GitHub and Dependabot do not execute them. No workflow writes changes to PR branches.

### GitHub Actions package feeds

The PR and push workflows set `PIP_INDEX_URL` to `https://packagefeedproxy.microsoft.io/pypi/simple/` for pip and nox installs. Their Node installation steps select `https://packagefeedproxy.microsoft.io/npm/` explicitly through `npm run ci:msft`, so these workflows do not set `NPM_CONFIG_REGISTRY`. CodeQL retains both environment variables because its autobuild step does not use the install helper. The npm endpoint follows the [TypeScript CI package-feed-proxy job](https://github.com/microsoft/TypeScript/blob/main/.github/workflows/ci.yml).

The shared lint and VSIX build actions and applicable PR/push test jobs use `npm run ci:msft`, rewriting working-copy lockfile URLs before installation and explicitly selecting the proxy for npm requests. Workflows do not commit these rewrites or change local registry configuration or Dependabot configuration. Existing dependency caches are retained; the VSIX build also retains its offline-cache preference. CodeQL retains workflow-level feed settings for its autobuild step.

### Azure release package feeds

The DevDiv Azure stable/prerelease pipelines pass the npm proxy to their build/publish templates using `customNPMRegistry`. After initializing submodules, their dependency install steps use `npm run ci:msft` to rewrite both npm lockfiles and install through the proxy.

Each Python bootstrap and bundled-library install step explicitly sets `PIP_INDEX_URL` from `PIP_PROXY_INDEX_URL`, selecting the pip proxy even when a template's authentication setup changes the job-level index. Release triggers, approval gates, signing, and publishing behavior are unchanged.

Dependabot continues to use public registries and its existing update settings. Legacy Azure stable, prerelease, and validation definitions are unchanged.
20 changes: 15 additions & 5 deletions build/azure-devdiv-pipeline.pre-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ variables:
value: VSCode-flake8
- name: VsixName
value: flake8.vsix
- name: AZURE_ARTIFACTS_FEED
value: 'https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/Pylance_PublicPackages/npm/registry/'
- name: NPM_CONFIG_REGISTRY
value: 'https://packagefeedproxy.microsoft.io/npm/'
- name: PIP_PROXY_INDEX_URL
value: 'https://packagefeedproxy.microsoft.io/pypi/simple/'

parameters:
- name: publishExtension
Expand All @@ -40,20 +42,28 @@ parameters:
default:
- script: git submodule update --init --recursive
displayName: Checkout submodules
- script: npm ci
- script: npm run ci:msft
displayName: Install NPM dependencies

- script: python -m pip install -U pip
displayName: Upgrade pip
env:
PIP_INDEX_URL: $(PIP_PROXY_INDEX_URL)

- script: python -m pip install wheel
displayName: Install wheel
env:
PIP_INDEX_URL: $(PIP_PROXY_INDEX_URL)

- script: python -m pip install nox
displayName: Install nox
env:
PIP_INDEX_URL: $(PIP_PROXY_INDEX_URL)

- script: python -m nox --session install_bundled_libs
displayName: Install Python dependencies
env:
PIP_INDEX_URL: $(PIP_PROXY_INDEX_URL)

- script: python ./build/update_ext_version.py --for-publishing
displayName: Update build number
Expand Down Expand Up @@ -85,7 +95,7 @@ extends:
buildSteps: ${{ parameters.buildSteps }}
isPreRelease: true
standardizedVersioning: true
customNPMRegistry: $(AZURE_ARTIFACTS_FEED)
customNPMRegistry: $(NPM_CONFIG_REGISTRY)

- stage: Publish
displayName: Publish Extension
Expand All @@ -99,4 +109,4 @@ extends:
ghCreateTag: true
ghCreateRelease: true
ghReleaseAddChangeLog: true
customNPMRegistry: $(AZURE_ARTIFACTS_FEED)
customNPMRegistry: $(NPM_CONFIG_REGISTRY)
20 changes: 15 additions & 5 deletions build/azure-devdiv-pipeline.stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ variables:
value: VSCode-flake8
- name: VsixName
value: flake8.vsix
- name: AZURE_ARTIFACTS_FEED
value: 'https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/Pylance_PublicPackages/npm/registry/'
- name: NPM_CONFIG_REGISTRY
value: 'https://packagefeedproxy.microsoft.io/npm/'
- name: PIP_PROXY_INDEX_URL
value: 'https://packagefeedproxy.microsoft.io/pypi/simple/'

parameters:
- name: publishExtension
Expand All @@ -35,20 +37,28 @@ parameters:
default:
- script: git submodule update --init --recursive
displayName: Checkout submodules
- script: npm ci
- script: npm run ci:msft
displayName: Install NPM dependencies

- script: python -m pip install -U pip
displayName: Upgrade pip
env:
PIP_INDEX_URL: $(PIP_PROXY_INDEX_URL)

- script: python -m pip install wheel
displayName: Install wheel
env:
PIP_INDEX_URL: $(PIP_PROXY_INDEX_URL)

- script: python -m pip install nox
displayName: Install nox
env:
PIP_INDEX_URL: $(PIP_PROXY_INDEX_URL)

- script: python -m nox --session install_bundled_libs
displayName: Install Python dependencies
env:
PIP_INDEX_URL: $(PIP_PROXY_INDEX_URL)

- script: python ./build/update_ext_version.py --release --for-publishing
displayName: Update build number
Expand Down Expand Up @@ -80,7 +90,7 @@ extends:
buildSteps: ${{ parameters.buildSteps }}
isPreRelease: false
standardizedVersioning: true
customNPMRegistry: $(AZURE_ARTIFACTS_FEED)
customNPMRegistry: $(NPM_CONFIG_REGISTRY)

- stage: WaitForValidation
displayName: Wait for Validation
Expand Down Expand Up @@ -108,4 +118,4 @@ extends:
ghCreateTag: true
ghCreateRelease: true
ghReleaseAddChangeLog: true
customNPMRegistry: $(AZURE_ARTIFACTS_FEED)
customNPMRegistry: $(NPM_CONFIG_REGISTRY)
112 changes: 112 additions & 0 deletions build/normalize-package-lock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

const { execFileSync, spawnSync } = require('node:child_process');
const { lstatSync, readFileSync, writeFileSync } = require('node:fs');
const path = require('node:path');
const { URL } = require('node:url');

const mirrorPrefix =
/^https:\/\/(?:(?:devdiv\.pkgs\.visualstudio\.com\/DevDiv|pkgs\.dev\.azure\.com\/devdiv\/DevDiv)\/_packaging\/Pylance_PublicPackages\/npm\/registry\/|packagefeedproxy\.microsoft\.io\/npm\/|ms-feed-\d+\.pkgs\.visualstudio\.com\/1es-public\/_packaging\/npm-public\/npm\/registry\/)/i;

function normalizePackageLock(source) {
const lockfile = JSON.parse(source);
if (
![2, 3].includes(lockfile.lockfileVersion) ||
!lockfile.packages ||
typeof lockfile.packages !== 'object' ||
Array.isArray(lockfile.packages)
) {
throw new Error('Expected an npm lockfile with lockfileVersion 2 or 3 and a packages section.');
}

const replacements = new Map();
function visit(value) {
if (!value || typeof value !== 'object') {
return;
}
if (typeof value.resolved === 'string' && mirrorPrefix.test(value.resolved)) {
const publicUrl = value.resolved.replace(mirrorPrefix, 'https://registry.npmjs.org/');
const url = new URL(publicUrl);
if (url.search || url.hash || !url.pathname.endsWith('.tgz')) {
throw new Error('Unexpected npm mirror tarball URL; refusing to rewrite it.');
}
if (typeof value.integrity !== 'string' || !value.integrity.trim()) {
throw new Error('Cannot normalize a mirrored package without an integrity hash.');
}
replacements.set(value.resolved, publicUrl);
}
for (const child of Object.values(value)) {
visit(child);
}
}
visit(lockfile.packages);
visit(lockfile.dependencies);

// Preserve formatting, dependency versions, and integrity hashes.
return source.replace(/("resolved"\s*:\s*)("(?:\\.|[^"\\])*")/g, (match, prefix, encodedUrl) => {
const replacement = replacements.get(JSON.parse(encodedUrl));
return replacement ? prefix + JSON.stringify(replacement) : match;
});
}

function normalizeStagedLockfile(cwd = process.cwd()) {
const git = (args) => execFileSync('git', args, { cwd, encoding: 'utf8' });
const staged = git(['diff', '--cached', '--name-only', '--diff-filter=ACMR', '-z', '--', 'package-lock.json']);
if (!staged.split('\0').includes('package-lock.json')) {
return false;
}
const source = git(['show', ':package-lock.json']);
if (normalizePackageLock(source) === source) {
return false;
}

const unstaged = spawnSync('git', ['diff', '--quiet', '--', 'package-lock.json'], { cwd, encoding: 'utf8' });
if (unstaged.error) {
throw unstaged.error;
}
if (unstaged.status === 1) {
throw new Error(
'package-lock.json has unstaged changes. Stage or stash them before committing; no changes were made.',
);
}
if (unstaged.status !== 0) {
throw new Error(`Unable to check unstaged lockfile changes: ${unstaged.stderr}`);
}

const file = path.join(cwd, 'package-lock.json');
if (!lstatSync(file).isFile()) {
throw new Error('Refusing to normalize a package-lock.json that is not a regular file.');
}
const workingSource = readFileSync(file, 'utf8');
writeFileSync(file, normalizePackageLock(workingSource), 'utf8');
git(['add', '--', 'package-lock.json']);
return true;
}

if (require.main === module) {
try {
const args = process.argv.slice(2);
if (args.length === 1 && args[0] === '--staged') {
if (normalizeStagedLockfile()) {
console.log('Normalized and re-staged package-lock.json registry URLs.');
}
} else if (args.length === 0) {
const source = readFileSync('package-lock.json', 'utf8');
const normalized = normalizePackageLock(source);
if (normalized !== source) {
writeFileSync('package-lock.json', normalized, 'utf8');
console.log('Normalized package-lock.json registry URLs. Stage the file before committing.');
} else {
console.log('package-lock.json registry URLs are already normalized.');
}
} else {
throw new Error('Usage: node build/normalize-package-lock.js [--staged]');
}
} catch (error) {
console.error(`Lockfile normalization failed: ${error.message}`);
process.exitCode = 1;
}
}

module.exports = { normalizePackageLock, normalizeStagedLockfile };
Loading
Loading