diff --git a/.github/workflows/localization.yml b/.github/workflows/localization.yml new file mode 100644 index 000000000..cc425be38 --- /dev/null +++ b/.github/workflows/localization.yml @@ -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' diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index d353976b9..49b43369f 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -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 }} @@ -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 }} diff --git a/.gitignore b/.gitignore index dddeeba2e..aba557387 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,8 @@ src/test/integration/test-workspace/**/pyenvs-*/ ai-artifacts/* # Code health analysis snapshots (generated, uploaded as artifacts) -analysis-snapshot.json \ No newline at end of file +analysis-snapshot.json + +# Localization is generated from the dev handback during releases. +/package.nls.*.json +/l10n/ diff --git a/README.md b/README.md index 286288de0..62b4124a2 100644 --- a/README.md +++ b/README.md @@ -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..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. diff --git a/build/azure-devdiv-pipeline.pre-release.yml b/build/azure-devdiv-pipeline.pre-release.yml index 1976fdea5..a13485b37 100644 --- a/build/azure-devdiv-pipeline.pre-release.yml +++ b/build/azure-devdiv-pipeline.pre-release.yml @@ -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 @@ -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 diff --git a/build/azure-devdiv-pipeline.stable.yml b/build/azure-devdiv-pipeline.stable.yml index b6df2b882..b1daa3ce5 100644 --- a/build/azure-devdiv-pipeline.stable.yml +++ b/build/azure-devdiv-pipeline.stable.yml @@ -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 @@ -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 diff --git a/src/features/envManagers.ts b/src/features/envManagers.ts index 6d0ed3998..6979d3341 100644 --- a/src/features/envManagers.ts +++ b/src/features/envManagers.ts @@ -206,6 +206,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers { * Only mutated by setEnvironment() / setEnvironments() / refreshEnvironment(). */ private readonly _activeSelection = new Map(); + 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 @@ -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'; } @@ -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); diff --git a/src/test/features/envManagers.unit.test.ts b/src/test/features/envManagers.unit.test.ts index a478d890a..482ecbd01 100644 --- a/src/test/features/envManagers.unit.test.ts +++ b/src/test/features/envManagers.unit.test.ts @@ -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();