diff --git a/.github/workflows/localization.yml b/.github/workflows/localization.yml new file mode 100644 index 00000000..47bbfa92 --- /dev/null +++ b/.github/workflows/localization.yml @@ -0,0 +1,30 @@ +name: Localization + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - package.json + - package.nls.json + - src/**/*.ts + - .github/workflows/localization.yml + +permissions: + contents: read + +concurrency: + group: localization-${{ github.ref }} + cancel-in-progress: false + +jobs: + localization: + if: github.repository == 'microsoft/vscode-python-debugger' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: microsoft/vscode-common-python-lsp/.github/actions/localization@d1877b84bb10ff7caa6bd12ab4e75498ba760790 + with: + app-id: ${{ vars.PYTHON_LOCALIZATION_APP_ID }} + private-key: ${{ secrets.PYTHON_LOCALIZATION_PRIVATE_KEY }} diff --git a/.gitignore b/.gitignore index d35af712..d82b0989 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,8 @@ bundled/libs/ # Generated files (sources live in pre-built/) l10n/bundle.l10n.*.json package.nls.*.json -telemetry.json \ No newline at end of file +telemetry.json + +# Localization is generated from the dev handback during releases. +/package.nls.*.json +/l10n/ diff --git a/README.md b/README.md index b16cca0a..b2d524cb 100644 --- a/README.md +++ b/README.md @@ -132,3 +132,15 @@ The DevDiv Azure stable/prerelease pipelines pass the npm proxy to their build/p 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. + +### 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.debugpy`**. English XLF is stored in `src/en/ms-python.debugpy` and handbacks in `out/ms-python.debugpy`. 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 (debugger pre-built assets remain available, but the release import runs after their overlay and is authoritative). + +Translation is asynchronous. All 14 supported locales must be present, and every required real-language table must contain translation evidence. Individual untranslated strings are allowed. Missing, incomplete, or wholly untranslated handbacks fail before packaging, without falling back to stale translations. Do not publish while onboarding or real translated output is pending. diff --git a/build/azure-devdiv-pipeline.pre-release.yml b/build/azure-devdiv-pipeline.pre-release.yml index 4d6c4c00..8a211c0f 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: d1877b84bb10ff7caa6bd12ab4e75498ba760790 + endpoint: Github-Pylance variables: - name: TeamName value: VSCode-python-debugger @@ -100,43 +106,16 @@ 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; we'll enable selectively per artifact output - 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: $(NPM_CONFIG_REGISTRY) - - - 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: $(NPM_CONFIG_REGISTRY) + buildPlatforms: ${{ parameters.buildPlatforms }} + buildSteps: ${{ parameters.buildSteps }} + standardizedVersioning: true + customNPMRegistry: $(NPM_CONFIG_REGISTRY) + 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 ad76e4fc..d0ff387d 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: d1877b84bb10ff7caa6bd12ab4e75498ba760790 + endpoint: Github-Pylance variables: - name: TeamName value: VSCode-python-debugger @@ -95,58 +101,17 @@ 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; we'll enable selectively per artifact output - 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: $(NPM_CONFIG_REGISTRY) - - - 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: $(NPM_CONFIG_REGISTRY) + buildPlatforms: ${{ parameters.buildPlatforms }} + buildSteps: ${{ parameters.buildSteps }} + standardizedVersioning: true + customNPMRegistry: $(NPM_CONFIG_REGISTRY) + publishExtension: ${{ parameters.publishExtension }} + teamName: $(TeamName) + ghCreateTag: true + ghCreateRelease: true + ghReleaseAddChangeLog: true + validationNotifyUsers: plseng@microsoft.com + sourceAnalysisPool: VSEng-MicroBuildVSStable + pullLocalization: true diff --git a/src/test/unittest/common/application/commands/reportIssueCommand.unit.test.ts b/src/test/unittest/common/application/commands/reportIssueCommand.unit.test.ts index 209b8937..327ffed4 100644 --- a/src/test/unittest/common/application/commands/reportIssueCommand.unit.test.ts +++ b/src/test/unittest/common/application/commands/reportIssueCommand.unit.test.ts @@ -12,19 +12,46 @@ import * as Telemetry from '../../../../../extension/telemetry/index'; import { EventName } from '../../../../../extension/telemetry/constants'; import * as vscodeapi from '../../../../../extension/common/vscodeapi'; import * as pythonApi from '../../../../../extension/common/python'; -import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../../constants'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, TEST_TIMEOUT } from '../../../../constants'; +import { EXTENSION_ROOT_DIR } from '../../../../../extension/common/constants'; import { openReportIssue } from '../../../../../extension/common/application/commands/reportIssueCommand'; import { PythonEnvironment } from '../../../../../extension/envExtApi'; suite('Report Issue Command', () => { let executeCommandStub: sinon.SinonStub; let resolveEnvironmentStub: sinon.SinonStub; + const templatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_template.md'); + const userTemplatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_user_data_template.md'); + let template: string; + let userTemplate: string; + let expectedIssueBody: string; + let expectedData: string; - setup(async () => { + suiteSetup(async function () { + this.timeout(TEST_TIMEOUT); + [template, userTemplate, expectedIssueBody, expectedData] = await Promise.all([ + fs.readFile(templatePath, 'utf8'), + fs.readFile(userTemplatePath, 'utf8'), + fs.readFile( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'resources', 'issueTemplate.md'), + 'utf8', + ), + fs.readFile( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'resources', 'issueUserDataTemplate.md'), + 'utf8', + ), + ]); + }); + + setup(() => { + const readFileStub = sinon.stub(fs, 'readFile').rejects(new Error('Unexpected template file read')); + readFileStub.withArgs(templatePath, 'utf8').resolves(template); + readFileStub.withArgs(userTemplatePath, 'utf8').resolves(userTemplate); executeCommandStub = sinon.stub(vscodeapi, 'executeCommand'); + sinon + .stub(pythonApi, 'getActiveEnvironmentPath') + .resolves({ id: '/path/to/interpreter', path: '/path/to/interpreter' }); resolveEnvironmentStub = sinon.stub(pythonApi, 'resolveEnvironment'); - // Ensure useEnvironmentsExtension is false for these tests - (pythonApi as any).useEnvironmentsExtension = false; const interpreter = { envId: { id: '/path/to/interpreter', @@ -37,30 +64,12 @@ suite('Report Issue Command', () => { teardown(() => { sinon.restore(); - (pythonApi as any).useEnvironmentsExtension = undefined; }); test('Test if issue body is filled correctly when including all the settings', async () => { executeCommandStub.withArgs('workbench.action.openIssueReporter', sinon.match.any).resolves(); await openReportIssue(); - - const issueTemplatePath = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'test', - 'resources', - 'issueTemplate.md', - ); - const expectedIssueBody = fs.readFileSync(issueTemplatePath, 'utf8'); - - const userDataTemplatePath = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'test', - 'resources', - 'issueUserDataTemplate.md', - ); - const expectedData = fs.readFileSync(userDataTemplatePath, 'utf8'); + sinon.assert.calledOnceWithExactly(resolveEnvironmentStub, '/path/to/interpreter'); sinon.assert.calledOnceWithExactly(executeCommandStub, 'workbench.action.openIssueReporter', sinon.match.any); diff --git a/src/test/unittest/configuration/resolvers/launch.unit.test.ts b/src/test/unittest/configuration/resolvers/launch.unit.test.ts index d8b46eda..8795b5b2 100644 --- a/src/test/unittest/configuration/resolvers/launch.unit.test.ts +++ b/src/test/unittest/configuration/resolvers/launch.unit.test.ts @@ -38,6 +38,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { let getOSTypeStub: sinon.SinonStub; let getWorkspaceFolderStub: sinon.SinonStub; let getInterpreterDetailsStub: sinon.SinonStub; + let getActiveEnvironmentPathStub: sinon.SinonStub; let getEnvFileStub: sinon.SinonStub; let getDebugEnvironmentVariablesStub: sinon.SinonStub; let getConfigurationStub: sinon.SinonStub; @@ -48,6 +49,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { getWorkspaceFolderStub = sinon.stub(vscodeapi, 'getWorkspaceFolders'); getOSTypeStub.returns(osType); getInterpreterDetailsStub = sinon.stub(pythonApi, 'getInterpreterDetails'); + getActiveEnvironmentPathStub = sinon.stub(pythonApi, 'getActiveEnvironmentPath'); getEnvFileStub = sinon.stub(settings, 'getEnvFile'); getDebugEnvironmentVariablesStub = sinon.stub(helper, 'getDebugEnvironmentVariables'); getConfigurationStub = sinon.stub(vscodeapi, 'getConfiguration'); @@ -87,6 +89,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { function setupIoc(pythonPath: string, workspaceFolder?: WorkspaceFolder) { getInterpreterDetailsStub.resolves({ path: [pythonPath] }); + getActiveEnvironmentPathStub.resolves({ id: pythonPath, path: pythonPath }); if (workspaceFolder) { getEnvFileStub @@ -868,7 +871,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const pythonFile = 'xyz.py'; setupIoc(pythonPath); setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - testsForRedirectOutput.forEach(async (testParams) => { + for (const testParams of testsForRedirectOutput) { const debugConfig = await resolveDebugConfiguration(workspaceFolder, { ...launch, console: testParams.console as ConsoleType, @@ -879,7 +882,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(debugConfig).to.have.property('debugOptions'); expect((debugConfig as DebugConfiguration).debugOptions).to.contain(DebugOptions.RedirectOutput); } - }); + } }); test('Test fixFilePathCase', async () => { @@ -956,13 +959,13 @@ getInfoPerOS().forEach(([osName, osType, path]) => { uriFormat: '%s', action: 'openExternally', }; - testsForautoStartBrowser.forEach(async (testParams) => { + for (const testParams of testsForautoStartBrowser) { const debugConfig = await resolveDebugConfiguration(workspaceFolder, { ...launch, ...testParams, }); - expect(debugConfig).to.have.property('serverReadyAction', expectedServerReadyAction); - }); + expect(debugConfig).to.have.deep.property('serverReadyAction', expectedServerReadyAction); + } }); test('Preserve serverReadyAction when already defined in configuration', async () => { @@ -978,7 +981,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { action: 'debugWithChrome', }; - testsForautoStartBrowser.forEach(async (testParams) => { + for (const testParams of testsForautoStartBrowser) { const debugConfig = await resolveDebugConfiguration(workspaceFolder, { ...launch, ...testParams, @@ -995,7 +998,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { uriFormat: '%s', action: 'openExternally', }); - }); + } }); test('Send consoleName value to debugpy as consoleTitle', async () => {