mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
feat: add allowlist compatibility check for API proposals in extensions (#289923)
This commit is contained in:
@@ -7,7 +7,7 @@ import { CosmosClient } from '@azure/cosmos';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { retry } from './retry.ts';
|
||||
import { type IExtensionManifest, parseApiProposalsFromSource, checkExtensionCompatibility } from './versionCompatibility.ts';
|
||||
import { type IExtensionManifest, parseApiProposalsFromSource, checkExtensionCompatibility, areAllowlistedApiProposalsMatching } from './versionCompatibility.ts';
|
||||
|
||||
const root = path.dirname(path.dirname(path.dirname(import.meta.dirname)));
|
||||
|
||||
@@ -75,6 +75,25 @@ async function checkCopilotChatCompatibility(): Promise<void> {
|
||||
|
||||
console.log(`Loaded ${proposalCount} API proposals from source`);
|
||||
|
||||
// Load product.json to check allowlisted API proposals
|
||||
const productJsonPath = path.join(root, 'product.json');
|
||||
let productJson;
|
||||
try {
|
||||
productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8'));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load or parse product.json: ${error}`);
|
||||
}
|
||||
const productAllowlistedProposals = productJson?.extensionEnabledApiProposals?.[extensionId];
|
||||
|
||||
if (productAllowlistedProposals) {
|
||||
console.log(`Product.json allowlisted proposals for ${extensionId}:`);
|
||||
for (const proposal of productAllowlistedProposals) {
|
||||
console.log(` ${proposal}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`Product.json allowlisted proposals for ${extensionId}: none`);
|
||||
}
|
||||
|
||||
// Fetch the latest extension manifest
|
||||
const manifest = await retry(() => fetchLatestExtensionManifest(extensionId));
|
||||
|
||||
@@ -92,6 +111,14 @@ async function checkCopilotChatCompatibility(): Promise<void> {
|
||||
if (manifest.enabledApiProposals?.length) {
|
||||
console.log(` ✓ API proposals compatible`);
|
||||
}
|
||||
|
||||
// Check that product.json allowlist matches package.json declarations
|
||||
const allowlistResult = areAllowlistedApiProposalsMatching(extensionId, productAllowlistedProposals, manifest.enabledApiProposals);
|
||||
if (!allowlistResult.compatible) {
|
||||
throw new Error(`Allowlist check failed:\n ${allowlistResult.errors.join('\n ')}`);
|
||||
}
|
||||
|
||||
console.log(` ✓ Product.json allowlist matches package.json`);
|
||||
console.log(`✓ ${extensionId} is compatible with this build`);
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,50 @@ export function parseApiProposalsFromSource(content: string): { [proposalName: s
|
||||
return allApiProposals;
|
||||
}
|
||||
|
||||
export function areAllowlistedApiProposalsMatching(
|
||||
extensionId: string,
|
||||
productAllowlistedProposals: string[] | undefined,
|
||||
manifestEnabledProposals: string[] | undefined
|
||||
): { compatible: boolean; errors: string[] } {
|
||||
// Normalize undefined to empty arrays for easier comparison
|
||||
const productProposals = productAllowlistedProposals || [];
|
||||
const manifestProposals = manifestEnabledProposals || [];
|
||||
|
||||
// If extension doesn't declare any proposals, it's always compatible
|
||||
// (product.json can allowlist more than the extension uses)
|
||||
if (manifestProposals.length === 0) {
|
||||
return { compatible: true, errors: [] };
|
||||
}
|
||||
|
||||
// If extension declares API proposals but product.json doesn't allowlist them
|
||||
if (productProposals.length === 0) {
|
||||
return {
|
||||
compatible: false,
|
||||
errors: [
|
||||
`Extension '${extensionId}' declares API proposals in package.json (${manifestProposals.join(', ')}) ` +
|
||||
`but product.json does not allowlist any API proposals for this extension`
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Check that all proposals in manifest are allowlisted in product.json
|
||||
// (product.json can have extra proposals that the extension doesn't use)
|
||||
// Note: Strip version suffixes from manifest proposals (e.g., "chatParticipant@2" -> "chatParticipant")
|
||||
// because product.json only contains base proposal names
|
||||
const productSet = new Set(productProposals);
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const proposal of manifestProposals) {
|
||||
// Strip version suffix if present (e.g., "chatParticipant@2" -> "chatParticipant")
|
||||
const proposalName = proposal.split('@')[0];
|
||||
if (!productSet.has(proposalName)) {
|
||||
errors.push(`API proposal '${proposal}' is declared in extension '${extensionId}' package.json but is not allowlisted in product.json`);
|
||||
}
|
||||
}
|
||||
|
||||
return { compatible: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
export function checkExtensionCompatibility(
|
||||
productVersion: string,
|
||||
productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>,
|
||||
@@ -343,5 +387,52 @@ export const allApiProposals = {
|
||||
|
||||
console.log(' ✓ checkExtensionCompatibility tests passed\n');
|
||||
|
||||
// areAllowlistedApiProposalsMatching tests
|
||||
console.log('Testing areAllowlistedApiProposalsMatching...');
|
||||
|
||||
// Both undefined - compatible
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', undefined, undefined).compatible, true);
|
||||
|
||||
// Both empty arrays - compatible
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', [], []).compatible, true);
|
||||
|
||||
// Exact match - compatible
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA', 'proposalB'], ['proposalA', 'proposalB']).compatible, true);
|
||||
|
||||
// Match regardless of order - compatible
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalB', 'proposalA'], ['proposalA', 'proposalB']).compatible, true);
|
||||
|
||||
// Extension declares but product.json doesn't allowlist - incompatible
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', undefined, ['proposalA']).compatible, false);
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', [], ['proposalA']).compatible, false);
|
||||
|
||||
// Product.json allowlists but extension doesn't declare - COMPATIBLE (product.json can have extras)
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA'], undefined).compatible, true);
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA'], []).compatible, true);
|
||||
|
||||
// Extension declares more than allowlisted - incompatible
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA'], ['proposalA', 'proposalB']).compatible, false);
|
||||
|
||||
// Product.json allowlists more than declared - COMPATIBLE (product.json can have extras)
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA', 'proposalB'], ['proposalA']).compatible, true);
|
||||
|
||||
// Completely different sets - incompatible (manifest has proposals not in allowlist)
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA'], ['proposalB']).compatible, false);
|
||||
|
||||
// Product.json has extras and manifest matches subset - compatible
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA', 'proposalB', 'proposalC'], ['proposalA', 'proposalB']).compatible, true);
|
||||
|
||||
// Versioned proposals - should strip version and match base name
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['chatParticipant'], ['chatParticipant@2']).compatible, true);
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA', 'proposalB'], ['proposalA@1', 'proposalB@3']).compatible, true);
|
||||
|
||||
// Versioned proposal not in allowlist - incompatible
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA'], ['proposalB@2']).compatible, false);
|
||||
|
||||
// Mix of versioned and unversioned proposals
|
||||
assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA', 'proposalB'], ['proposalA', 'proposalB@2']).compatible, true);
|
||||
|
||||
console.log(' ✓ areAllowlistedApiProposalsMatching tests passed\n');
|
||||
|
||||
console.log('All tests passed! ✓');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user