diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 126b180fa35..286d0752701 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -207,6 +207,7 @@ export interface IProductConfiguration { readonly excludeVersionRange?: string; }>; readonly extensionsForceVersionByQuality?: readonly string[]; + readonly autoUpdateBuiltinExtensions?: readonly string[]; readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 426df59122b..04763c87d10 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -331,11 +331,16 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio }; try { + const systemExtensions = await this.getInstalled(ExtensionType.System); // Start installing extensions for (const { manifest, extension, options } of extensions) { - const isApplicationScoped = options.isApplicationScoped || options.isBuiltin || isApplicationScopedExtension(manifest); + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + const isSystemExtension = systemExtensions.some(e => areSameExtensions(e.identifier, { id: extensionId })); + const isBuiltin = options.isBuiltin || isSystemExtension; + const isApplicationScoped = options.isApplicationScoped || isBuiltin || isApplicationScopedExtension(manifest); const installExtensionTaskOptions: InstallExtensionTaskOptions = { ...options, + isBuiltin, isApplicationScoped, profileLocation: isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation(), productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 6414a467547..76dc4bbca23 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -1000,6 +1000,15 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle return false; } + // For auto-update extensions defined in product, only allow versions with same major.minor as the product version + if (this.productService.autoUpdateBuiltinExtensions?.some(id => id.toLowerCase() === extension.id.toLowerCase())) { + const productMajorMinor = `${semver.major(productVersion.version)}.${semver.minor(productVersion.version)}`; + const extensionMajorMinor = `${semver.major(extension.version)}.${semver.minor(extension.version)}`; + if (productMajorMinor !== extensionMajorMinor) { + return false; + } + } + // Specific version if (isString(version)) { if (extension.version !== version) { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index ab11f6bb950..2b5b6cc14c8 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -286,6 +286,7 @@ export interface ILocalExtension extends IExtension { preRelease: boolean; updated: boolean; pinned: boolean; + autoUpdate: boolean; source: InstallSource; size: number; } diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 37eac451a86..25859d91dd9 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -55,6 +55,7 @@ interface IRelaxedScannedExtension { isValid: boolean; validations: readonly [Severity, string][]; preRelease: boolean; + autoUpdate: boolean; } export type IScannedExtension = Readonly & { manifest: IExtensionManifest }; @@ -351,6 +352,9 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } private dedupExtensions(system: IScannedExtension[] | undefined, user: IScannedExtension[] | undefined, development: IScannedExtension[] | undefined, targetPlatform: TargetPlatform, pickLatest: boolean): IScannedExtension[] { + const autoUpdateBuiltinExtensions = this.productService.autoUpdateBuiltinExtensions; + const productVersion = autoUpdateBuiltinExtensions?.length ? this.getProductVersion() : undefined; + const productMajorMinor = productVersion ? `${semver.major(productVersion.version)}.${semver.minor(productVersion.version)}` : undefined; const pick = (existing: IScannedExtension, extension: IScannedExtension, isDevelopment: boolean): boolean => { if (!isDevelopment) { if (existing.metadata?.isApplicationScoped && !extension.metadata?.isApplicationScoped) { @@ -399,7 +403,18 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem this.logService.debug(`Skipping obsolete system extension ${extension.location.path}.`); return; } + if (productMajorMinor && autoUpdateBuiltinExtensions?.some(id => id.toLowerCase() === extension.identifier.id.toLowerCase())) { + const extensionMajorMinor = `${semver.major(extension.manifest.version)}.${semver.minor(extension.manifest.version)}`; + if (productMajorMinor !== extensionMajorMinor) { + this.logService.info(`Skipping auto-update builtin extension ${extension.identifier.id} with version ${extension.manifest.version} because it does not match the product version ${productVersion.version}`); + return; + } + } if (!existing || pick(existing, extension, false)) { + // Mark as builtin when extension is an auto-update builtin extension + if (autoUpdateBuiltinExtensions?.some(id => id.toLowerCase() === extension.identifier.id.toLowerCase())) { + extension = { ...extension, isBuiltin: true }; + } result.set(extension.identifier.id, extension); } }); @@ -563,6 +578,8 @@ type NlsConfiguration = { class ExtensionsScanner extends Disposable { private readonly extensionsEnabledWithApiProposalVersion: string[]; + private readonly productQuality: string | undefined; + private readonly autoUpdateBuiltinExtensions: ReadonlySet; constructor( @IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @@ -574,6 +591,8 @@ class ExtensionsScanner extends Disposable { ) { super(); this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? []; + this.productQuality = productService.quality; + this.autoUpdateBuiltinExtensions = new Set(productService.autoUpdateBuiltinExtensions?.map(id => id.toLowerCase()) ?? []); } async scanExtensions(input: ExtensionScannerInput): Promise { @@ -712,6 +731,7 @@ class ExtensionsScanner extends Disposable { isValid, validations, preRelease: !!metadata?.preRelease, + autoUpdate: type === ExtensionType.System && this.autoUpdateBuiltinExtensions.has(id.toLowerCase()) && this.productQuality === 'stable', }; if (input.validate) { extension = this.validate(extension, input); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index cef0d3c59c1..90aad936789 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -549,6 +549,8 @@ export class ExtensionsScanner extends Disposable { @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IProductService private readonly productService: IProductService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @ILogService private readonly logService: ILogService, ) { super(); @@ -558,6 +560,7 @@ export class ExtensionsScanner extends Disposable { async cleanUp(): Promise { await this.removeTemporarilyDeletedFolders(); + await this.removeStaleAutoUpdateBuiltinExtensions(); await this.deleteExtensionsMarkedForRemoval(); //TODO: Remove this initiialization after coupe of releases await this.initializeExtensionSize(); @@ -914,6 +917,7 @@ export class ExtensionsScanner extends Disposable { installedTimestamp: extension.metadata?.installedTimestamp, updated: !!extension.metadata?.updated, pinned: !!extension.metadata?.pinned, + autoUpdate: extension.autoUpdate, private: !!extension.metadata?.private, isWorkspaceScoped: false, source: extension.metadata?.source ?? (extension.identifier.uuid ? 'gallery' : 'vsix'), @@ -932,6 +936,28 @@ export class ExtensionsScanner extends Disposable { })); } + private async removeStaleAutoUpdateBuiltinExtensions(): Promise { + const autoUpdateBuiltinExtensions = this.productService.autoUpdateBuiltinExtensions; + if (!autoUpdateBuiltinExtensions?.length) { + return; + } + const productVersion = this.productService.version; + const productMajorMinor = `${semver.major(productVersion)}.${semver.minor(productVersion)}`; + const extensions = await this.extensionsScannerService.scanAllUserExtensions(); + const staleExtensions = extensions.filter(extension => { + if (!autoUpdateBuiltinExtensions.some(id => id.toLowerCase() === extension.identifier.id.toLowerCase())) { + return false; + } + const extensionMajorMinor = `${semver.major(extension.manifest.version)}.${semver.minor(extension.manifest.version)}`; + return productMajorMinor !== extensionMajorMinor; + }); + if (staleExtensions.length) { + this.logService.info('Removing stale auto-update builtin extensions:', staleExtensions.map(e => `${e.identifier.id}@${e.manifest.version}`).join(', ')); + await this.extensionsProfileScannerService.removeExtensionsFromProfile(staleExtensions.map(e => e.identifier), this.userDataProfilesService.defaultProfile.extensionsResource); + await Promise.allSettled(staleExtensions.map(e => this.deleteExtension(e, 'stale auto-update builtin'))); + } + } + private async deleteExtensionsMarkedForRemoval(): Promise { let removed: IStringDictionary; try { diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts index 415cdcb9775..f7164918773 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { newWriteableBufferStream } from '../../../../base/common/buffer.js'; import { joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { isUUID } from '../../../../base/common/uuid.js'; @@ -11,16 +12,20 @@ import { mock } from '../../../../base/test/common/mock.js'; import { IConfigurationService } from '../../../configuration/common/configuration.js'; import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; import { IEnvironmentService } from '../../../environment/common/environment.js'; -import { IRawGalleryExtensionVersion, sortExtensionVersions, filterLatestExtensionVersionsForTargetPlatform } from '../../common/extensionGalleryService.js'; +import { IAllowedExtensionsService, IGalleryExtension } from '../../common/extensionManagement.js'; +import { IExtensionGalleryManifestService } from '../../common/extensionGalleryManifest.js'; +import { AbstractExtensionGalleryService, ExtensionGalleryServiceWithNoStorageService, IRawGalleryExtensionVersion, sortExtensionVersions, filterLatestExtensionVersionsForTargetPlatform } from '../../common/extensionGalleryService.js'; import { IFileService } from '../../../files/common/files.js'; import { FileService } from '../../../files/common/fileService.js'; import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js'; -import { NullLogService } from '../../../log/common/log.js'; +import { TestInstantiationService } from '../../../instantiation/test/common/instantiationServiceMock.js'; +import { ILogService, NullLogService } from '../../../log/common/log.js'; import product from '../../../product/common/product.js'; import { IProductService } from '../../../product/common/productService.js'; +import { IRequestService } from '../../../request/common/request.js'; import { resolveMarketplaceHeaders } from '../../../externalServices/common/marketplace.js'; import { InMemoryStorageService, IStorageService } from '../../../storage/common/storage.js'; -import { TelemetryConfiguration, TELEMETRY_SETTING_ID } from '../../../telemetry/common/telemetry.js'; +import { ITelemetryService, TelemetryConfiguration, TELEMETRY_SETTING_ID } from '../../../telemetry/common/telemetry.js'; import { TargetPlatform } from '../../../extensions/common/extensions.js'; import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; @@ -459,3 +464,103 @@ suite('Extension Gallery Service', () => { }); }); + +suite('Extension Gallery Service - Auto Update Builtin Extensions', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + function createGalleryService(autoUpdateBuiltinExtensions: string[], productVersion: string = '1.66.0'): AbstractExtensionGalleryService { + const instantiationService = disposables.add(new TestInstantiationService()); + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider('vscode-tests', fileSystemProvider)); + instantiationService.stub(ILogService, logService); + instantiationService.stub(IFileService, fileService); + instantiationService.stub(IRequestService, { async request() { return { res: { statusCode: 200 }, stream: newWriteableBufferStream() }; } }); + instantiationService.stub(IEnvironmentService, new EnvironmentServiceMock(joinPath(URI.file('tests').with({ scheme: 'vscode-tests' }), 'machineid'))); + instantiationService.stub(ITelemetryService, NullTelemetryService); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IAllowedExtensionsService, { isAllowed: () => true }); + instantiationService.stub(IExtensionGalleryManifestService, { extensionGalleryManifest: undefined, extensionGalleryManifestStatus: 0 }); + instantiationService.stub(IProductService, { + _serviceBrand: undefined, + version: productVersion, + autoUpdateBuiltinExtensions, + }); + return disposables.add(instantiationService.createInstance(ExtensionGalleryServiceWithNoStorageService)); + } + + function aGalleryExtension(id: string, version: string): IGalleryExtension { + const [publisher, name] = id.split('.'); + return { + identifier: { id, uuid: id }, + name, + version, + publisher, + publisherDisplayName: publisher, + allTargetPlatforms: [TargetPlatform.UNDEFINED], + properties: { + isPreReleaseVersion: false, + targetPlatform: TargetPlatform.UNDEFINED, + engine: undefined, + enabledApiProposals: undefined, + }, + assets: { manifest: null }, + } as unknown as IGalleryExtension; + } + + test('extension with matching major.minor is compatible', async () => { + const galleryService = createGalleryService(['pub.name'], '1.66.0'); + const extension = aGalleryExtension('pub.name', '1.66.1'); + + const result = await galleryService.isExtensionCompatible(extension, false, TargetPlatform.UNDEFINED, { version: '1.66.0' }); + + assert.strictEqual(result, true); + }); + + test('extension with mismatched major.minor is not compatible', async () => { + const galleryService = createGalleryService(['pub.name'], '1.66.0'); + const extension = aGalleryExtension('pub.name', '1.67.0'); + + const result = await galleryService.isExtensionCompatible(extension, false, TargetPlatform.UNDEFINED, { version: '1.66.0' }); + + assert.strictEqual(result, false); + }); + + test('extension not in autoUpdateBuiltinExtensions is not version-restricted', async () => { + const galleryService = createGalleryService(['pub.other'], '1.66.0'); + const extension = aGalleryExtension('pub.name', '1.67.0'); + + const result = await galleryService.isExtensionCompatible(extension, false, TargetPlatform.UNDEFINED, { version: '1.66.0' }); + + assert.strictEqual(result, true); + }); + + test('extension with same major but different minor is not compatible', async () => { + const galleryService = createGalleryService(['pub.name'], '1.66.0'); + const extension = aGalleryExtension('pub.name', '1.65.5'); + + const result = await galleryService.isExtensionCompatible(extension, false, TargetPlatform.UNDEFINED, { version: '1.66.0' }); + + assert.strictEqual(result, false); + }); + + test('version check is case insensitive for extension id', async () => { + const galleryService = createGalleryService(['Pub.Name'], '1.66.0'); + const extension = aGalleryExtension('pub.name', '1.67.0'); + + const result = await galleryService.isExtensionCompatible(extension, false, TargetPlatform.UNDEFINED, { version: '1.66.0' }); + + assert.strictEqual(result, false); + }); + + test('no version restriction when autoUpdateBuiltinExtensions is empty', async () => { + const galleryService = createGalleryService([], '1.66.0'); + const extension = aGalleryExtension('pub.name', '1.67.0'); + + const result = await galleryService.isExtensionCompatible(extension, false, TargetPlatform.UNDEFINED, { version: '1.66.0' }); + + assert.strictEqual(result, true); + }); +}); diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index b4729bbd940..528b03e9a63 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -340,6 +340,114 @@ suite('NativeExtensionsScanerService Test', () => { function anExtensionManifest(manifest: Partial): Partial { return { engines: { vscode: '^1.66.0' }, version: '1.0.0', main: 'main.js', activationEvents: ['*'], ...manifest }; } + + suite('auto update builtin extensions', () => { + + test('scan user extension with matching product version is included', async () => { + instantiationService.stub(IProductService, { version: '1.66.0', quality: 'stable', autoUpdateBuiltinExtensions: ['pub.name'] }); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.66.1' })); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: false }); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].manifest.version, '1.66.1'); + }); + + test('scan user extension with mismatched product version is excluded', async () => { + instantiationService.stub(IProductService, { version: '1.66.0', quality: 'stable', autoUpdateBuiltinExtensions: ['pub.name'] }); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.67.0' })); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: false }); + + assert.deepStrictEqual(actual.length, 0); + }); + + test('scan user extension not in autoUpdateBuiltinExtensions is not filtered', async () => { + instantiationService.stub(IProductService, { version: '1.66.0', quality: 'stable', autoUpdateBuiltinExtensions: ['pub.other'] }); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.67.0' })); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: false }); + + assert.deepStrictEqual(actual.length, 1); + }); + + test('scan picks matching version when multiple versions exist', async () => { + instantiationService.stub(IProductService, { version: '1.66.0', quality: 'stable', autoUpdateBuiltinExtensions: ['pub.name'] }); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.66.1' })); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.67.0' })); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: false }); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].manifest.version, '1.66.1'); + }); + + test('scan all extensions prefers matching user extension over system extension', async () => { + instantiationService.stub(IProductService, { version: '1.66.0', quality: 'stable', autoUpdateBuiltinExtensions: ['pub.name'] }); + await aSystemExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.66.0' })); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.66.1' })); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanAllExtensions({}, { profileLocation: instantiationService.get(IUserDataProfilesService).defaultProfile.extensionsResource, includeInvalid: false }); + + const extension = actual.find(e => e.identifier.id === 'pub.name'); + assert.ok(extension); + assert.deepStrictEqual(extension.manifest.version, '1.66.1'); + assert.deepStrictEqual(extension.isBuiltin, true); + }); + + test('scan all extensions falls back to system extension when user extension has mismatched version', async () => { + instantiationService.stub(IProductService, { version: '1.66.0', quality: 'stable', autoUpdateBuiltinExtensions: ['pub.name'] }); + await aSystemExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.66.0' })); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.67.0' })); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanAllExtensions({}, { profileLocation: instantiationService.get(IUserDataProfilesService).defaultProfile.extensionsResource, includeInvalid: false }); + + const extension = actual.find(e => e.identifier.id === 'pub.name'); + assert.ok(extension); + assert.deepStrictEqual(extension.manifest.version, '1.66.0'); + assert.deepStrictEqual(extension.type, ExtensionType.System); + }); + + test('system extension has autoUpdate set to true when in autoUpdateBuiltinExtensions and quality is stable', async () => { + instantiationService.stub(IProductService, { version: '1.66.0', quality: 'stable', autoUpdateBuiltinExtensions: ['pub.name'] }); + await aSystemExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanSystemExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].autoUpdate, true); + }); + + test('system extension has autoUpdate set to false when not in autoUpdateBuiltinExtensions', async () => { + instantiationService.stub(IProductService, { version: '1.66.0', quality: 'stable', autoUpdateBuiltinExtensions: ['pub.other'] }); + await aSystemExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanSystemExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].autoUpdate, false); + }); + + test('system extension has autoUpdate set to false when quality is not stable', async () => { + instantiationService.stub(IProductService, { version: '1.66.0', quality: 'insider', autoUpdateBuiltinExtensions: ['pub.name'] }); + await aSystemExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanSystemExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].autoUpdate, false); + }); + + }); }); suite('ExtensionScannerInput', () => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index de710641876..71ec8015cd9 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1959,8 +1959,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Skip if check updates only for builtin extensions and current extension is not builtin. continue; } - if (installed.isBuiltin && !installed.local?.pinned && (installed.type === ExtensionType.System || !installed.local?.identifier.uuid)) { - // Skip checking updates for a builtin extension if it is a system extension or if it does not has Marketplace identifier + if (!installed.local?.autoUpdate && installed.isBuiltin && !installed.local?.pinned && (installed.type === ExtensionType.System || !installed.local?.identifier.uuid)) { + // Skip checking updates for a builtin extension if it is a system extension or if it does not have a Marketplace identifier continue; } if (installed.local?.source === 'resource') { @@ -2235,6 +2235,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return false; } + if (extension.local?.autoUpdate) { + // Extensions marked for auto-update are always auto-updated + return true; + } + const autoUpdateValue = this.getAutoUpdateValue(); if (autoUpdateValue === false) { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index ea7f3641853..030f0fbfb2f 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -1412,6 +1412,7 @@ class WorkspaceExtensionsManagementService extends Disposable { installedTimestamp: extension.metadata?.installedTimestamp, updated: !!extension.metadata?.updated, pinned: !!extension.metadata?.pinned, + autoUpdate: false, isWorkspaceScoped: true, private: false, source: 'resource', diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 35a2f0592d3..7ec290eb06b 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -261,6 +261,7 @@ function toLocalExtension(extension: IExtension): ILocalExtension { targetPlatform: TargetPlatform.WEB, updated: !!metadata.updated, pinned: !!metadata?.pinned, + autoUpdate: false, private: !!metadata.private, isWorkspaceScoped: false, source: metadata?.source ?? (extension.identifier.uuid ? 'gallery' : 'resource'),