Skip to content
Open
32 changes: 32 additions & 0 deletions .github/workflows/localization.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Localization

on:
workflow_dispatch:
push:
branches:
- main
paths:
- package.json
- package.nls.json
- src/**/*.ts
- .nvmrc
- .github/workflows/localization.yml

permissions:
contents: read

concurrency:
group: localization-${{ github.ref }}
cancel-in-progress: false

jobs:
localization:
if: github.repository == 'microsoft/vscode-python-environments'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: microsoft/vscode-common-python-lsp/.github/actions/localization@6b4a3573e7e7a9fe1e468e129544432278aec2d6
with:
app-id: ${{ vars.PYTHON_LOCALIZATION_APP_ID }}
private-key: ${{ secrets.PYTHON_LOCALIZATION_PRIVATE_KEY }}
node-version: '22.21.1'
24 changes: 24 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,20 @@ jobs:

- name: Run E2E Tests (non-Linux)
if: runner.os != 'Linux'
id: e2e-non-linux
env:
VSCODE_TEST_DIAGNOSTICS_DIR: ${{ runner.temp }}/vscode-test-diagnostics
run: npm run e2e-test

- name: Upload E2E failure diagnostics (non-Linux)
if: ${{ failure() && steps.e2e-non-linux.outcome == 'failure' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: e2e-${{ matrix.os }}-${{ matrix.python-version }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/vscode-test-diagnostics
if-no-files-found: error
retention-days: 7

integration-tests:
name: Integration Tests
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -409,8 +421,20 @@ jobs:

- name: Run Integration Tests (non-Linux)
if: runner.os != 'Linux'
id: integration-non-linux
env:
VSCODE_TEST_DIAGNOSTICS_DIR: ${{ runner.temp }}/vscode-test-diagnostics
run: npm run integration-test

- name: Upload integration failure diagnostics (non-Linux)
if: ${{ failure() && steps.integration-non-linux.outcome == 'failure' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: integration-${{ matrix.os }}-${{ matrix.python-version }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/vscode-test-diagnostics
if-no-files-found: error
retention-days: 7

integration-tests-multiroot:
name: Integration Tests (Multi-Root)
runs-on: ${{ matrix.os }}
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ src/test/integration/test-workspace/**/pyenvs-*/
ai-artifacts/*

# Code health analysis snapshots (generated, uploaded as artifacts)
analysis-snapshot.json
analysis-snapshot.json

# Localization is generated from the dev handback during releases.
/package.nls.*.json
/l10n/
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,15 @@ This project may contain trademarks or logos for projects, products, or services
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
Any use of third-party trademarks or logos are subject to those third-party's policies.

### Localization

The **Localization** GitHub workflow submits English strings using the [shared composite action and Python tooling](https://github.com/microsoft/vscode-common-python-lsp#shared-localization-automation). Stable and pre-release DevDiv pipelines independently retrieve translations through Pylance-Eng's `pullLocalization: true`; releases never submit English or create translation-update PRs.

Both paths use `microsoft/vscode-python-extensions-loc`, branch **`dev`**, with canonical extension ID **`ms-python.vscode-python-envs`**. English XLF is stored in `src/en/ms-python.vscode-python-envs` and handbacks in `out/ms-python.vscode-python-envs`. The action and Azure tooling checkout are pinned to the same reviewed revision.

Configure the `PYTHON_LOCALIZATION_APP_ID` Actions variable and `PYTHON_LOCALIZATION_PRIVATE_KEY` secret here, or grant access to organization-level credentials. The app needs Contents write access only to the localization drop; the caller's repository token stays read-only. The shared action uses the Microsoft npm proxy and pinned localization CLI. Authorize the literal `Github-Pylance` service connection for both Azure repository resources and release pipelines.

Keep English `package.nls.json` tracked. Root `package.nls.<locale>.json` and `l10n/` runtime files are generated, git-ignored release outputs. VSCE must include generated locale files despite Git ignores. Local builds are English-only unless translations are imported explicitly.

Translation is asynchronous. Nonempty manifest and runtime tables must be present for all 14 supported locales. Untranslated strings, including wholly untranslated tables, are allowed; users see English until translations arrive. Missing, empty, malformed, or incomplete handbacks still fail before packaging, without falling back to stale translations.
71 changes: 24 additions & 47 deletions build/azure-devdiv-pipeline.pre-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,21 @@ schedules:

resources:
repositories:
- repository: MicroBuildTemplate
type: git
name: 1ESPipelineTemplates/MicroBuildTemplate
ref: refs/tags/release

- repository: templates
type: git
name: DevDiv/Pylance-Eng
ref: refs/heads/main
- repository: translations
type: github
name: microsoft/vscode-python-extensions-loc
# Temporarily use dev while the localization force-push issue is resolved.
ref: refs/heads/dev
endpoint: Github-Pylance
- repository: localizationTools
type: github
name: microsoft/vscode-common-python-lsp
ref: 6b4a3573e7e7a9fe1e468e129544432278aec2d6
endpoint: Github-Pylance

variables:
- name: TeamName
Expand Down Expand Up @@ -78,46 +84,17 @@ parameters:
displayName: Build extension

extends:
template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate
template: azure-pipelines/extension/pre-release.yml@templates
parameters:
sdl:
sourceAnalysisPool: VSEng-MicroBuildVSStable
codeSignValidation:
enabled: true
sbom:
enabled: false # Disable global SBOM generation; enable it on the signed artifact.
pool:
name: AzurePipelines-EO
os: windows

customBuildTags:
- ES365AIMigrationTooling

stages:
- stage: Build
displayName: Build & Package Extension
jobs:
- template: azure-pipelines/extension/templates/jobs/package.yml@templates
parameters:
buildPlatforms: ${{ parameters.buildPlatforms }}
buildSteps: ${{ parameters.buildSteps }}
isPreRelease: true
standardizedVersioning: true
customNPMRegistry: $(AZURE_ARTIFACTS_FEED)
nodeVersion: $(NodeVersion)

- stage: Publish
displayName: Publish Extension
dependsOn: Build
jobs:
- template: azure-pipelines/extension/templates/jobs/publish-extension.yml@templates
parameters:
buildPlatforms: ${{ parameters.buildPlatforms }}
publishExtension: ${{ parameters.publishExtension }}
preRelease: true
teamName: $(TeamName)
ghCreateTag: true
ghCreateRelease: true
ghReleaseAddChangeLog: true
customNPMRegistry: $(AZURE_ARTIFACTS_FEED)
nodeVersion: $(NodeVersion)
buildPlatforms: ${{ parameters.buildPlatforms }}
buildSteps: ${{ parameters.buildSteps }}
standardizedVersioning: true
customNPMRegistry: $(AZURE_ARTIFACTS_FEED)
nodeVersion: $(NodeVersion)
publishExtension: ${{ parameters.publishExtension }}
teamName: $(TeamName)
ghCreateTag: true
ghCreateRelease: true
ghReleaseAddChangeLog: true
sourceAnalysisPool: VSEng-MicroBuildVSStable
pullLocalization: true
87 changes: 25 additions & 62 deletions build/azure-devdiv-pipeline.stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@ pr: none

resources:
repositories:
- repository: MicroBuildTemplate
type: git
name: 1ESPipelineTemplates/MicroBuildTemplate
ref: refs/tags/release

- repository: templates
type: git
name: DevDiv/Pylance-Eng
ref: refs/heads/main
- repository: translations
type: github
name: microsoft/vscode-python-extensions-loc
# Temporarily use dev while the localization force-push issue is resolved.
ref: refs/heads/dev
endpoint: Github-Pylance
- repository: localizationTools
type: github
name: microsoft/vscode-common-python-lsp
ref: 6b4a3573e7e7a9fe1e468e129544432278aec2d6
endpoint: Github-Pylance

variables:
- name: TeamName
Expand Down Expand Up @@ -73,61 +79,18 @@ parameters:
displayName: Build extension

extends:
template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate
template: azure-pipelines/extension/stable.yml@templates
parameters:
sdl:
sourceAnalysisPool: VSEng-MicroBuildVSStable
codeSignValidation:
enabled: true
sbom:
enabled: false # Disable global SBOM generation; enable it on the signed artifact.
pool:
name: AzurePipelines-EO
os: windows

customBuildTags:
- ES365AIMigrationTooling

stages:
- stage: Build
displayName: Build & Package Extension
jobs:
- template: azure-pipelines/extension/templates/jobs/package.yml@templates
parameters:
buildPlatforms: ${{ parameters.buildPlatforms }}
buildSteps: ${{ parameters.buildSteps }}
isPreRelease: false
standardizedVersioning: true
customNPMRegistry: $(AZURE_ARTIFACTS_FEED)
nodeVersion: $(NodeVersion)

- stage: WaitForValidation
displayName: Wait for Validation
dependsOn: Build
jobs:
- job: wait_for_validation
displayName: Wait for manual validation
pool: server
steps:
- task: ManualValidation@0
timeoutInMinutes: 1440 # task times out in 1 day
inputs:
notifyUsers: 'plseng@microsoft.com'
instructions: 'please test the latest draft release then publish it.'
onTimeout: reject

- stage: Publish
displayName: Publish Extension
dependsOn: WaitForValidation
jobs:
- template: azure-pipelines/extension/templates/jobs/publish-extension.yml@templates
parameters:
buildPlatforms: ${{ parameters.buildPlatforms }}
publishExtension: ${{ parameters.publishExtension }}
preRelease: false
teamName: $(TeamName)
ghCreateTag: true
ghCreateRelease: true
ghReleaseAddChangeLog: true
customNPMRegistry: $(AZURE_ARTIFACTS_FEED)
nodeVersion: $(NodeVersion)
buildPlatforms: ${{ parameters.buildPlatforms }}
buildSteps: ${{ parameters.buildSteps }}
standardizedVersioning: true
customNPMRegistry: $(AZURE_ARTIFACTS_FEED)
nodeVersion: $(NodeVersion)
publishExtension: ${{ parameters.publishExtension }}
teamName: $(TeamName)
ghCreateTag: true
ghCreateRelease: true
ghReleaseAddChangeLog: true
validationNotifyUsers: plseng@microsoft.com
sourceAnalysisPool: VSEng-MicroBuildVSStable
pullLocalization: true
21 changes: 19 additions & 2 deletions src/features/envManagers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
* Only mutated by setEnvironment() / setEnvironments() / refreshEnvironment().
*/
private readonly _activeSelection = new Map<string, PythonEnvironment | undefined>();
private _globalSelectionManagerId: string | undefined;
/**
* The last explicit selection for each scope, keyed like `_activeSelection`: the manager that stores
* it, and whether the user made it. Explicit selections are user selections and environments chosen
Expand Down Expand Up @@ -1130,16 +1131,25 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
): 'recorded' | 'cleared' | 'kept' | 'unchanged' {
if (environment && explicit && manager.id !== INLINE_SCRIPT_MANAGER_ID) {
this._explicitSelections.set(key, { managerId: manager.id, userSelected });
if (key === 'global') {
this._globalSelectionManagerId = manager.id;
}
return 'recorded';
}
const current = this._explicitSelections.get(key);
if (!current) {
if (key === 'global') {
this._globalSelectionManagerId = manager.id;
}
return 'unchanged';
}
if (!userSelected && current.userSelected) {
return 'kept';
}
this._explicitSelections.delete(key);
if (key === 'global') {
this._globalSelectionManagerId = getDefaultEnvManagerSetting(this.pm, undefined);
}
return 'cleared';
}

Expand Down Expand Up @@ -1239,8 +1249,15 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
? this._environmentManagers.get(cachedEnv.envId.managerId)
: undefined;

if (context === undefined && cachedManager && this._explicitSelections.has('global')) {
return cachedManager;
if (context === undefined && this._explicitSelections.has('global')) {
// A manager may return another manager's fallback environment; route through
// the manager that selected it, not the owner of the announced descriptor.
const globalManager = this._globalSelectionManagerId
? this._environmentManagers.get(this._globalSelectionManagerId)
: cachedManager;
if (globalManager) {
return globalManager;
}
}

const defaultEnvManagerId = getDefaultEnvManagerSetting(this.pm, context);
Expand Down
18 changes: 18 additions & 0 deletions src/test/features/envManagers.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,24 @@ suite('PythonEnvironmentManagers - selection stored by a non-default manager', (
assert.strictEqual(envManagers.getLastKnownEnvironment(undefined)?.envId.id, newestGlobal.envId.id);
});

test('global refresh follows the configured manager after an explicit selection is cleared', async () => {
await envManagers.setEnvironments('global', configured, false, { explicit: true });
await envManagers.setEnvironments('global', configured, false);
await flush();
const changeEvents: DidChangeEnvironmentEventArgs[] = [];
envManagers.onDidChangeActiveEnvironment((e) => changeEvents.push(e));
venvGet.resolves(localVenv);

await envManagers.refreshEnvironment(undefined);
await flush();

assert.strictEqual((await envManagers.getEnvironment(undefined))?.envId.id, localVenv.envId.id);
assert.deepStrictEqual(
changeEvents.map((e) => [e.old?.envId.id, e.new?.envId.id]),
[[newestGlobal.envId.id, localVenv.envId.id]],
);
});

test('clearing an explicit project selection publishes the environment that reads now return', async () => {
await envManagers.setEnvironment(projectUri, configured, false, { explicit: true });
await flush();
Expand Down
Loading