From fcbabe4c7359d5c8fc6e1cd1b732ddbd60f8f161 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:07:47 -0700 Subject: [PATCH] Merge extension-provided policy (#306874) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add merge script for extension configuration policies from distro Adds build/lib/policies/mergeExtensionPolicies.ts which fetches the distro's product.json (at the commit pinned in package.json) and merges extensionConfigurationPolicy entries into policyData.jsonc. The --export-policy-data step only captures policies from the OSS configuration registry. Extension policies (e.g., Copilot settings) are defined in vscode-distro's product.json and are not available in the OSS dev build. This merge script bridges that gap. Workflow: ./scripts/code.sh --export-policy-data && node build/lib/policies/mergeExtensionPolicies.ts Also updates: - policyExport integration test to run the merge step - add-policy skill documentation with extension policy lifecycle * Simplify extension policy format: drop localization block from distro - mergeExtensionPolicies.ts now reads only description/category from the distro and synthesizes localization key/value pairs internally - Integration test gracefully skips merge step when distro is unavailable - Updated SKILL.md to document the simplified distro format * docs: use gh auth token in policy export command * Handle old distro format missing description/category fields * docs: add troubleshooting section for merge step in policy skill * Require description and category fields in extension policy entries * Add extension configuration policies to policyData.jsonc * Address PR feedback: fix disclaimer, log targetPath, strip GITHUB_TOKEN from test * fix: revert extension policies from policyData.jsonc, remove console.log from test Extension policies should only be added after the distro PR merges. Test runner disallows console output in tests. * Add mock distro test for mergeExtensionPolicies, remove merge from export test - mergeExtensionPolicies.ts: add DISTRO_PRODUCT_JSON env var to override the distro path (for testing without private repo access) - New test: creates a mock product.json, runs the merge script, verifies policies are added correctly and idempotently - Export test: no longer runs the merge step (avoids needing distro access or GITHUB_TOKEN in CI) * Update distro and regenerate policyData.jsonc with extension policies * Fix CI: add extension policy fixture for export test The export test now runs mergeExtensionPolicies using a checked-in fixture file instead of requiring distro access or GITHUB_TOKEN. When extension policies change in the distro, regenerate both policyData.jsonc and the fixture file. * Integrate extension policy merge into --export-policy-data - Removed mergeExtensionPolicies.ts — no separate CLI tool needed - --export-policy-data now reads build/lib/policies/extensionPolicies.json and merges extension policies into the output automatically - Single command workflow: ./scripts/code.sh --export-policy-data - Simplified test — no merge step, no GITHUB_TOKEN, no distro access - Updated SKILL.md to document the simplified workflow * Dynamic distro reading: --export-policy-data fetches from distro directly - policyExport.contribution.ts reads distro product.json dynamically: 1. DISTRO_PRODUCT_JSON env var (for testing) 2. .build/distro/mixin/stable/product.json (local checkout) 3. GitHub API with GITHUB_TOKEN (remote fetch) - New scripts/export-policy-data.sh wrapper: sets up GITHUB_TOKEN via gh CLI and runs the export - Deleted build/lib/policies/extensionPolicies.json (no static copy) - Test uses DISTRO_PRODUCT_JSON with a fixture file - Uses sandbox process.env and VSBuffer (renderer-safe) * Replace bash wrapper with cross-platform TS script - New build/lib/policies/exportPolicyData.ts: handles transpilation, GITHUB_TOKEN setup via gh CLI, and runs --export-policy-data - Added 'npm run export-policy-data' script to package.json - Removed scripts/export-policy-data.sh - Updated SKILL.md and test error message * Use OAuth device flow for GitHub token acquisition exportPolicyData.ts now acquires tokens via: 1. GITHUB_TOKEN env var (if set) 2. gh CLI (fast, non-interactive) 3. GitHub OAuth device flow (interactive, no dependencies) Based on vscode-copilot-chat's getToken.mts pattern. * Fix error messages to reference npm run export-policy-data * Fix disclaimer to reference npm run export-policy-data, clean up stale refs * Clarify test fixture is static and not expected to track distro changes * Add inline comment in test clarifying fixture is static * SKILL.md: add step to update distro commit hash in package.json * Remove inline comment per PR feedback * Remove local .build/distro/ fallback path per PR feedback --- .github/skills/add-policy/SKILL.md | 51 ++++++++- build/lib/policies/exportPolicyData.ts | 85 +++++++++++++++ build/lib/policies/policyData.jsonc | 58 +++++++++- package.json | 1 + .../policyExport.contribution.ts | 101 +++++++++++++++++- .../test/node/extensionPolicyFixture.json | 28 +++++ .../test/node/policyExport.integrationTest.ts | 11 +- 7 files changed, 328 insertions(+), 7 deletions(-) create mode 100644 build/lib/policies/exportPolicyData.ts create mode 100644 src/vs/workbench/contrib/policyExport/test/node/extensionPolicyFixture.json diff --git a/.github/skills/add-policy/SKILL.md b/.github/skills/add-policy/SKILL.md index 64119b056ba..40f1cc3a376 100644 --- a/.github/skills/add-policy/SKILL.md +++ b/.github/skills/add-policy/SKILL.md @@ -124,15 +124,62 @@ npm run compile-check-ts-native Regenerate the auto-generated policy catalog: ```bash -npm run transpile-client && ./scripts/code.sh --export-policy-data +npm run export-policy-data ``` +This script handles transpilation, sets up `GITHUB_TOKEN` (via `gh` CLI or GitHub OAuth device flow), and runs `--export-policy-data`. The export command reads extension configuration policies from the distro's `product.json` via the GitHub API and merges them into the output. + This updates `build/lib/policies/policyData.jsonc`. **Never edit this file manually.** Verify your new policy appears in the output. You will need code review from a codeowner to merge the change to main. ## Policy for extension-provided settings -For an extension author to provide policies for their extension's settings, a change must be made in `vscode-distro` to the `product.json`. +Extension authors cannot add `policy:` fields directly—their settings are defined in the extension's `package.json`, not in VS Code core. Instead, policies for extension settings are defined in `vscode-distro`'s `product.json` under the `extensionConfigurationPolicy` key. + +### How it works + +1. **Source of truth**: The `extensionConfigurationPolicy` map lives in `vscode-distro` under `mixin/{quality}/product.json` (stable, insider, exploration). +2. **Runtime**: When VS Code starts with a distro-mixed `product.json`, `configurationExtensionPoint.ts` reads `extensionConfigurationPolicy` and attaches matching `policy` objects to extension-contributed configuration properties. +3. **Export/build**: The `--export-policy-data` command fetches the distro's `product.json` at the commit pinned in `package.json` and merges extension policies into the output. Use `npm run export-policy-data` which sets up authentication automatically. + +### Distro format + +Each entry in `extensionConfigurationPolicy` must include: + +```json +"extensionConfigurationPolicy": { + "publisher.extension.settingName": { + "name": "PolicyName", + "category": "InteractiveSession", + "minimumVersion": "1.99", + "description": "Human-readable description." + } +} +``` + +- `name`: PascalCase policy name, unique across all policies +- `category`: Must be a valid `PolicyCategory` enum value (e.g., `InteractiveSession`, `Extensions`) +- `minimumVersion`: The VS Code version that first shipped this policy +- `description`: Human-readable description string used to generate localization key/value pairs for ADMX/ADML/macOS/Linux policy artifacts + +### Adding a new extension policy + +1. Add the entry to `extensionConfigurationPolicy` in **all three** quality `product.json` files in `vscode-distro` (`mixin/stable/`, `mixin/insider/`, `mixin/exploration/`) +2. Update the `distro` commit hash in `package.json` to point to the distro commit that includes your new entry — the export command fetches extension policies from the pinned distro commit +3. Regenerate `policyData.jsonc` by running `npm run export-policy-data` (see Step 4 above) +4. Update the test fixture at `src/vs/workbench/contrib/policyExport/test/node/extensionPolicyFixture.json` with the new entry + +### Test fixtures + +The file `src/vs/workbench/contrib/policyExport/test/node/extensionPolicyFixture.json` is a test fixture that must stay in sync with the extension policies in the checked-in `policyData.jsonc`. When extension policies are added or changed in the distro, this fixture must be updated to match — otherwise the integration test will fail because the test output (generated from the fixture) won't match the checked-in file (generated from the real distro). + +### Downstream consumers + +| Consumer | What it reads | Output | +|----------|--------------|--------| +| `policyGenerator.ts` | `policyData.jsonc` | ADMX/ADML (Windows GP), `.mobileconfig` (macOS), `policy.json` (Linux) | +| `vscode-website` (`gulpfile.policies.js`) | `policyData.jsonc` | Enterprise policy reference table at code.visualstudio.com/docs/enterprise/policies | +| `vscode-docs` | Generated from website build | `docs/enterprise/policies.md` | ## Examples diff --git a/build/lib/policies/exportPolicyData.ts b/build/lib/policies/exportPolicyData.ts new file mode 100644 index 00000000000..eaaaeb60cb6 --- /dev/null +++ b/build/lib/policies/exportPolicyData.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { execSync, execFileSync } from 'child_process'; +import { resolve } from 'path'; + +const rootPath = resolve(import.meta.dirname, '..', '..', '..'); + +// VS Code OAuth app client ID (same as the GitHub Authentication extension) +const CLIENT_ID = '01ab8ac9400c4e429b23'; + +/** + * Acquires a GitHub token via the OAuth device flow. + * Opens the browser for the user to authorize, then polls for the token. + */ +async function acquireTokenViaDeviceFlow(): Promise { + const response1 = await (await fetch('https://github.com/login/device/code', { + method: 'POST', + body: JSON.stringify({ client_id: CLIENT_ID, scope: 'repo' }), + headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, + })).json() as { user_code: string; device_code: string; verification_uri: string; expires_in: number; interval: number }; + + console.log(`\n Copy this code: ${response1.user_code}`); + console.log(` Then open: ${response1.verification_uri}`); + console.log(` Waiting for authorization (up to ${response1.expires_in}s)...\n`); + + let expiresIn = response1.expires_in; + while (expiresIn > 0) { + await new Promise(resolve => setTimeout(resolve, 1000 * response1.interval)); + expiresIn -= response1.interval; + + const response2 = await (await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + body: JSON.stringify({ + client_id: CLIENT_ID, + device_code: response1.device_code, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + }), + headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, + })).json() as { access_token?: string }; + + if (response2.access_token) { + return response2.access_token; + } + } + + throw new Error('Timed out waiting for GitHub authorization'); +} + +// Ensure sources are transpiled +console.log('Transpiling client sources...'); +execSync('npm run transpile-client', { cwd: rootPath, stdio: 'inherit' }); + +// Set up GITHUB_TOKEN if not already set +if (!process.env['GITHUB_TOKEN'] && !process.env['DISTRO_PRODUCT_JSON']) { + // Try gh CLI first (fast, non-interactive) + let token: string | undefined; + try { + token = execFileSync('gh', ['auth', 'token'], { encoding: 'utf8' }).trim(); + console.log('Set GITHUB_TOKEN from gh CLI.'); + } catch { + // Fall back to OAuth device flow (interactive) + console.log('gh CLI not available, starting GitHub OAuth device flow...'); + token = await acquireTokenViaDeviceFlow(); + console.log('GitHub authorization successful.'); + } + + process.env['GITHUB_TOKEN'] = token; +} + +// Run the export +console.log('Exporting policy data...'); +const codeScript = process.platform === 'win32' + ? resolve(rootPath, 'scripts', 'code.bat') + : resolve(rootPath, 'scripts', 'code.sh'); + +execSync(`"${codeScript}" --export-policy-data`, { + cwd: rootPath, + stdio: 'inherit', + env: process.env, +}); + +console.log('\nPolicy data exported to build/lib/policies/policyData.jsonc'); diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index f58dda70afa..23e21914f05 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -1,4 +1,4 @@ -/** THIS FILE IS AUTOMATICALLY GENERATED USING `code --export-policy-data`. DO NOT MODIFY IT MANUALLY. **/ +/** THIS FILE IS AUTOMATICALLY GENERATED USING `npm run export-policy-data`. DO NOT MODIFY IT MANUALLY. **/ { "categories": [ { @@ -314,6 +314,62 @@ }, "type": "boolean", "default": false + }, + { + "key": "github.copilot.nextEditSuggestions.enabled", + "name": "CopilotNextEditSuggestions", + "category": "InteractiveSession", + "minimumVersion": "1.99", + "localization": { + "description": { + "key": "github.copilot.nextEditSuggestions.enabled", + "value": "Whether to enable next edit suggestions (NES). NES can propose a next edit based on your recent changes." + } + }, + "type": "boolean", + "default": true + }, + { + "key": "github.copilot.chat.reviewSelection.enabled", + "name": "CopilotReviewSelection", + "category": "InteractiveSession", + "minimumVersion": "1.104", + "localization": { + "description": { + "key": "github.copilot.chat.reviewSelection.enabled", + "value": "Enables code review on current selection." + } + }, + "type": "boolean", + "default": true + }, + { + "key": "github.copilot.chat.reviewAgent.enabled", + "name": "CopilotReviewAgent", + "category": "InteractiveSession", + "minimumVersion": "1.104", + "localization": { + "description": { + "key": "github.copilot.chat.reviewAgent.enabled", + "value": "Enables the code review agent." + } + }, + "type": "boolean", + "default": true + }, + { + "key": "github.copilot.chat.claudeAgent.enabled", + "name": "Claude3PIntegration", + "category": "InteractiveSession", + "minimumVersion": "1.113", + "localization": { + "description": { + "key": "github.copilot.chat.claudeAgent.enabled", + "value": "Enable Claude Agent sessions in VS Code. Start and resume agentic coding sessions powered by Anthropic Claude Agent SDK directly in the editor. Uses your existing Copilot subscription." + } + }, + "type": "boolean", + "default": true } ] } diff --git a/package.json b/package.json index 6b3e88bdee9..ac972ae2a2f 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "valid-layers-check": "node build/checker/layersChecker.ts && tsgo --project build/checker/tsconfig.browser.json && tsgo --project build/checker/tsconfig.worker.json && tsgo --project build/checker/tsconfig.node.json && tsgo --project build/checker/tsconfig.electron-browser.json && tsgo --project build/checker/tsconfig.electron-main.json && tsgo --project build/checker/tsconfig.electron-utility.json", "define-class-fields-check": "node build/lib/propertyInitOrderChecker.ts && tsgo --project src/tsconfig.defineClassFields.json", "update-distro": "node build/npm/update-distro.ts", + "export-policy-data": "node build/lib/policies/exportPolicyData.ts", "web": "echo 'npm run web' is replaced by './scripts/code-server' or './scripts/code-web'", "compile-cli": "npm run gulp compile-cli", "compile-web": "npm run gulp compile-web", diff --git a/src/vs/workbench/contrib/policyExport/electron-browser/policyExport.contribution.ts b/src/vs/workbench/contrib/policyExport/electron-browser/policyExport.contribution.ts index 51a0f8b42e1..e78e85ab2b4 100644 --- a/src/vs/workbench/contrib/policyExport/electron-browser/policyExport.contribution.ts +++ b/src/vs/workbench/contrib/policyExport/electron-browser/policyExport.contribution.ts @@ -8,6 +8,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { INativeEnvironmentService } from '../../../../platform/environment/common/environment.js'; +import { process } from '../../../../base/parts/sandbox/electron-browser/globals.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; @@ -20,6 +21,13 @@ import { PolicyCategory, PolicyCategoryData } from '../../../../base/common/poli import { ExportedPolicyDataDto } from '../common/policyDto.js'; import { join } from '../../../../base/common/path.js'; +interface ExtensionConfigurationPolicyEntry { + readonly name: string; + readonly category: string; + readonly minimumVersion: `${number}.${number}`; + readonly description: string; +} + export class PolicyExportContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.policyExport'; static readonly DEFAULT_POLICY_EXPORT_PATH = 'build/lib/policies/policyData.jsonc'; @@ -96,7 +104,38 @@ export class PolicyExportContribution extends Disposable implements IWorkbenchCo } this.log(`Discovered ${policyData.policies.length} policies to export.`); - const disclaimerComment = `/** THIS FILE IS AUTOMATICALLY GENERATED USING \`code --export-policy-data\`. DO NOT MODIFY IT MANUALLY. **/`; + // Merge extension configuration policies from the distro's product.json. + // Checks DISTRO_PRODUCT_JSON env var (for testing), + // then falls back to fetching from GitHub API with GITHUB_TOKEN. + const distroProduct = await this.getDistroProductJson(); + const extensionPolicies = distroProduct['extensionConfigurationPolicy'] as Record | undefined; + if (extensionPolicies) { + const existingKeys = new Set(policyData.policies.map(p => p.key)); + let added = 0; + for (const [key, entry] of Object.entries(extensionPolicies)) { + if (existingKeys.has(key)) { + continue; + } + if (!entry.description || !entry.category) { + throw new Error(`Extension policy '${key}' is missing required 'description' or 'category' field.`); + } + policyData.policies.push({ + key, + name: entry.name, + category: entry.category, + minimumVersion: entry.minimumVersion, + localization: { + description: { key, value: entry.description }, + }, + type: 'boolean', + default: true, + }); + added++; + } + this.log(`Merged ${added} extension configuration policies.`); + } + + const disclaimerComment = `/** THIS FILE IS AUTOMATICALLY GENERATED USING \`npm run export-policy-data\`. DO NOT MODIFY IT MANUALLY. **/`; const policyDataFileContent = `${disclaimerComment}\n${JSON.stringify(policyData, null, 4)}\n`; await this.fileService.writeFile(URI.file(policyDataPath), VSBuffer.fromString(policyDataFileContent)); this.log(`Successfully exported ${policyData.policies.length} policies to ${policyDataPath}.`); @@ -108,6 +147,66 @@ export class PolicyExportContribution extends Disposable implements IWorkbenchCo await this.nativeHostService.exit(1); } } + + /** + * Reads the distro product.json for the 'stable' quality. + * Checks DISTRO_PRODUCT_JSON env var (for testing), + * then falls back to fetching from the GitHub API using GITHUB_TOKEN. + */ + private async getDistroProductJson(): Promise> { + const root = this.nativeEnvironmentService.appRoot; + + // 1. DISTRO_PRODUCT_JSON env var (for testing) + const envPath = process.env['DISTRO_PRODUCT_JSON']; + if (envPath) { + this.log(`Reading distro product.json from DISTRO_PRODUCT_JSON=${envPath}`); + const content = (await this.fileService.readFile(URI.file(envPath))).value.toString(); + return JSON.parse(content); + } + + // 2. GitHub API with GITHUB_TOKEN + const packageJsonPath = join(root, 'package.json'); + const packageJsonContent = (await this.fileService.readFile(URI.file(packageJsonPath))).value.toString(); + const packageJson = JSON.parse(packageJsonContent); + const distroCommit: string | undefined = packageJson.distro; + + if (!distroCommit) { + throw new Error( + 'No distro commit found in package.json. ' + + 'Use `npm run export-policy-data` which sets up the required environment.' + ); + } + + const token = process.env['GITHUB_TOKEN']; + if (!token) { + throw new Error( + 'GITHUB_TOKEN is required to fetch distro product.json. ' + + 'Use `npm run export-policy-data` which sets up the required environment.' + ); + } + + this.log(`Fetching distro product.json for commit ${distroCommit} from GitHub...`); + const url = `https://api.github.com/repos/microsoft/vscode-distro/contents/mixin/stable/product.json?ref=${encodeURIComponent(distroCommit)}`; + const response = await fetch(url, { + headers: { + 'Accept': 'application/vnd.github+json', + 'Authorization': `Bearer ${token}`, + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'VSCode Build' + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch distro product.json: ${response.status} ${response.statusText}`); + } + + const data = await response.json() as { content: string; encoding: string }; + if (data.encoding !== 'base64') { + throw new Error(`Unexpected encoding from GitHub API: ${data.encoding}`); + } + const content = VSBuffer.wrap(Uint8Array.from(atob(data.content), c => c.charCodeAt(0))).toString(); + return JSON.parse(content); + } } registerWorkbenchContribution2( diff --git a/src/vs/workbench/contrib/policyExport/test/node/extensionPolicyFixture.json b/src/vs/workbench/contrib/policyExport/test/node/extensionPolicyFixture.json new file mode 100644 index 00000000000..d11df1b5594 --- /dev/null +++ b/src/vs/workbench/contrib/policyExport/test/node/extensionPolicyFixture.json @@ -0,0 +1,28 @@ +{ + "extensionConfigurationPolicy": { + "github.copilot.nextEditSuggestions.enabled": { + "name": "CopilotNextEditSuggestions", + "minimumVersion": "1.99", + "category": "InteractiveSession", + "description": "Whether to enable next edit suggestions (NES). NES can propose a next edit based on your recent changes." + }, + "github.copilot.chat.reviewSelection.enabled": { + "name": "CopilotReviewSelection", + "minimumVersion": "1.104", + "category": "InteractiveSession", + "description": "Enables code review on current selection." + }, + "github.copilot.chat.reviewAgent.enabled": { + "name": "CopilotReviewAgent", + "minimumVersion": "1.104", + "category": "InteractiveSession", + "description": "Enables the code review agent." + }, + "github.copilot.chat.claudeAgent.enabled": { + "name": "Claude3PIntegration", + "minimumVersion": "1.113", + "category": "InteractiveSession", + "description": "Enable Claude Agent sessions in VS Code. Start and resume agentic coding sessions powered by Anthropic Claude Agent SDK directly in the editor. Uses your existing Copilot subscription." + } + } +} diff --git a/src/vs/workbench/contrib/policyExport/test/node/policyExport.integrationTest.ts b/src/vs/workbench/contrib/policyExport/test/node/policyExport.integrationTest.ts index c9cc8881d24..f11fa239343 100644 --- a/src/vs/workbench/contrib/policyExport/test/node/policyExport.integrationTest.ts +++ b/src/vs/workbench/contrib/policyExport/test/node/policyExport.integrationTest.ts @@ -47,10 +47,15 @@ suite('PolicyExport Integration Tests', () => { ? join(rootPath, 'scripts', 'code.bat') : join(rootPath, 'scripts', 'code.sh'); - // Skip prelaunch to avoid redownloading electron while the parent VS Code is using it + // Skip prelaunch to avoid redownloading electron while the parent VS Code is using it. + // DISTRO_PRODUCT_JSON points to a static test fixture so --export-policy-data can + // merge extension policies without needing distro access or GITHUB_TOKEN. + // This fixture is NOT expected to stay in sync with the distro — it exists purely + // to test the generation code path. Policy values will drift and that is fine. + const fixturePath = join(rootPath, 'src/vs/workbench/contrib/policyExport/test/node/extensionPolicyFixture.json'); await exec(`"${scriptPath}" --export-policy-data="${tempFile}"`, { cwd: rootPath, - env: { ...process.env, VSCODE_SKIP_PRELAUNCH: '1' } + env: { ...process.env, VSCODE_SKIP_PRELAUNCH: '1', DISTRO_PRODUCT_JSON: fixturePath } }); // Read both files @@ -63,7 +68,7 @@ suite('PolicyExport Integration Tests', () => { assert.strictEqual( exportedContent, checkedInContent, - 'Exported policy data should match the checked-in file. If this fails, run: ./scripts/code.sh --export-policy-data' + 'Exported policy data should match the checked-in file. If this fails, run: npm run export-policy-data' ); } finally { // Clean up temp file