Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
name: CI
on:
pull_request:
branches:
- 'main'
workflow_call:
inputs:
ensure_stable_cli:
description: 'Set to true to enforce stable CLI release check'
required: false
type: boolean
default: false

jobs:
build:
Expand Down Expand Up @@ -39,8 +43,12 @@ jobs:
sudo apt-get install xvfb
xvfb-run --auto-servernum npm run test:integration
if: runner.os == 'Linux'
env:
ENSURE_STABLE_CLI: ${{ inputs.ensure_stable_cli && '1' || '0' }}
- name: Run integration tests
run: npm run test:integration
if: runner.os != 'Linux'
env:
ENSURE_STABLE_CLI: ${{ inputs.ensure_stable_cli && '1' || '0' }}


4 changes: 2 additions & 2 deletions .github/workflows/release-preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ on:
- main
jobs:
build:
uses: snyk/vscode-extension/.github/workflows/ci.yaml@main
uses: ./.github/workflows/ci.yaml

release-preview:
name: Release Preview
runs-on: ubuntu-latest
Expand Down
8 changes: 3 additions & 5 deletions .github/workflows/release-stable.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ name: Build and Release

on:
workflow_dispatch:
branches:
- main
Copy link
Contributor Author

@rrama rrama Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, I removed this because you cannot set branches on workflow_dispatch.

# schedule: TODO: align release schedule with CLI
# - cron: '0 9 * * 2' # every Tuesday at 9 am UTC

jobs:
build:
uses: snyk/vscode-extension/.github/workflows/ci.yaml@main
uses: ./.github/workflows/ci.yaml
with:
ensure_stable_cli: true

release:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
- Ensure the Snyk Language Server Protocol version is correct in the plugin.
- `PROTOCOL_VERSION` in `src/snyk/common/constants/languageServer.ts`

- There must be a stable CLI release for the Snyk Language Server Protocol version before you can release this extension, once there is update the hardcoded flag, which switches CLI tests over to the stable channel.
- `isStableCLIReleased` in `src/snyk/common/constants/languageServer.ts`
- You cannot release without this, the release pipeline has a special test that will fail.

**Initiate Release**

- If you want to do a hotfix with a subset of commits from main, create a hotfix branch off the previous release tag.
Expand Down
4 changes: 4 additions & 0 deletions src/snyk/common/constants/languageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
export const SNYK_LANGUAGE_SERVER_NAME = 'Snyk Language Server';
// The internal language server protocol version for custom messages and configuration
export const PROTOCOL_VERSION = 21;
// Flag to indicate if a stable CLI has been released for the current protocol version
// Set to true once a stable CLI release exists for this protocol version
// During development of a new protocol version, this should be false
export const isStableCLIReleased = true;

// LS protocol methods (needed for not having to rely on vscode dependencies in testing)
export const DID_CHANGE_CONFIGURATION_METHOD = 'workspace/didChangeConfiguration';
Expand Down
6 changes: 6 additions & 0 deletions src/snyk/common/languageServer/languageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ export class LanguageServer implements ILanguageServer {
// Starts the language server and the client. LS will be downloaded if missing.
// Returns a promise that resolves when the language server is ready to receive requests.
async start(): Promise<void> {
// Skip during integration tests to prevent LS from interfering with tests
if (process.env.SNYK_INTEGRATION_TEST_MODE === 'true') {
this.logger.info('Skipping Language Server start in integration test mode');
return;
}

// wait until Snyk LS is downloaded
await firstValueFrom(this.downloadService.downloadReady$);
this.logger.info('Starting Snyk Language Server');
Expand Down
5 changes: 5 additions & 0 deletions src/snyk/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,11 @@ class SnykExtension extends SnykLib implements IExtension {
}

public initDependencyDownload(): DownloadService {
// Skip during integration tests, as we do not need to download the CLI an extra time.
if (process.env.SNYK_INTEGRATION_TEST_MODE === 'true') {
return this.downloadService;
}

this.downloadService.downloadOrUpdate().catch(err => {
void ErrorHandler.handleGlobal(err, Logger, this.contextService, this.loadingBadge);
void this.notificationService.showErrorNotification((err as Error).message);
Expand Down
9 changes: 7 additions & 2 deletions src/test/integration/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,19 @@ suite('Configuration', () => {
}
workspaceFolder = folders[0];

// Clear state before config changes so the watcher doesn't skip them due to stale state
LanguageServer.clearLSUpdatingOrgState();

await clearOrganizationAtAllLevels();
await configuration.setFolderConfigs([]);

LanguageServer.clearLSUpdatingOrgState();
});

teardown(async () => {
await sleep(200); // Allow VS Code time to fully work out its workspace situation before cleanup

// Clear state before config changes so the watcher doesn't skip them due to stale state
LanguageServer.clearLSUpdatingOrgState();

await clearOrganizationAtAllLevels();
await configuration.setFolderConfigs([]);

Expand All @@ -117,6 +121,7 @@ suite('Configuration', () => {
}
}

// Clear again after cleanup to ensure clean state for next test
LanguageServer.clearLSUpdatingOrgState();
});

Expand Down
61 changes: 54 additions & 7 deletions src/test/integration/staticCliApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Configuration } from '../../snyk/common/configuration/configuration';
import { VSCodeWorkspace } from '../../snyk/common/vscode/workspace';
import { ILog, LogLevel } from '../../snyk/common/logger/interfaces';
import { CliSupportedPlatform } from '../../snyk/cli/supportedPlatforms';
import { isStableCLIReleased } from '../../snyk/common/constants/languageServer';
import { Readable } from 'stream';
import * as fs from 'fs';
import * as path from 'path';
Expand All @@ -14,15 +15,26 @@ suite('StaticCliApi - Integration Tests', function () {
// Set a longer timeout for network operations
this.timeout(30000); // 30 seconds

// Use preview channel if stable isn't released yet
const defaultCLIReleaseChannel = isStableCLIReleased ? 'stable' : 'preview';

let firstSetup = true;
let api: StaticCliApi;
let workspace: VSCodeWorkspace;
let configuration: Configuration;
let logger: ILog;

setup(() => {
setup(async () => {
workspace = new VSCodeWorkspace();
configuration = new Configuration(process.env, workspace);

// Set the test default CLI release channel in configuration
await configuration.setCliReleaseChannel(defaultCLIReleaseChannel);
if (firstSetup) {
console.log(`Test suite using ${defaultCLIReleaseChannel} CLI channel where unspecified`);
firstSetup = false;
}

// Create a simple logger implementation
logger = {
info: (message: unknown) => console.log(`[INFO] ${message}`),
Expand All @@ -36,21 +48,56 @@ suite('StaticCliApi - Integration Tests', function () {
api = new StaticCliApi(workspace, configuration, logger);
});

test('getLatestCliVersion returns a valid version string', async () => {
test('Release pipeline check: ensure stable CLI is released when ENSURE_STABLE_CLI is set', function () {
// This test enforces that we don't release without a stable CLI
// The ENSURE_STABLE_CLI env var should be set in the release pipeline
if (process.env.ENSURE_STABLE_CLI === '1') {
ok(
isStableCLIReleased,
'You must update `isStableCLIReleased` to true before a stable release of the VS Code extension',
);
} else {
// Not in release pipeline, this test passes
ok(true, 'Not in release pipeline (ENSURE_STABLE_CLI not set), skipping check');
}
});

test('getLatestCliVersion with stable channel returns a valid version string', async function () {
// Skip this test if no stable CLI has been released yet for the current protocol version
if (!isStableCLIReleased) {
console.log(
'Skipping stable channel test: No stable CLI released yet for current protocol version (expected during development)',
);
this.skip();
}
const version = await api.getLatestCliVersion('stable');

// Version should be in format like "1.1234.0" or "v1.1234.0"
ok(version, 'Version should not be empty');
ok(/^v?\d+\.\d+\.\d+/.test(version), `Version "${version}" should match semantic version pattern`);
});

test('getLatestCliVersion with preview channel returns a version', async () => {
test('getLatestCliVersion with preview channel returns a valid version string', async () => {
const version = await api.getLatestCliVersion('preview');

ok(version, 'Preview version should not be empty');
ok(/^v?\d+\.\d+\.\d+/.test(version), `Preview version "${version}" should match semantic version pattern`);
});

test('getLatestCliVersion with stable channel should fail when isStableCLIReleased is false', async function () {
// Only run this test if no stable CLI has been released yet
if (isStableCLIReleased) {
console.log('Skipping test: stable CLI is already released');
this.skip();
}

// Attempting to get stable CLI version should fail when none exists for the protocol version
await rejects(
api.getLatestCliVersion('stable'),
'Should throw an error when stable CLI does not exist for current protocol version. If stable is released, update isStableCLIReleased flag.',
);
});

test('downloadBinary downloads actual CLI binary stream', async () => {
// Test with Linux platform as it's smaller than other binaries
const platform: CliSupportedPlatform = 'linux';
Expand Down Expand Up @@ -96,7 +143,7 @@ suite('StaticCliApi - Integration Tests', function () {

test('getSha256Checksum returns valid checksum', async () => {
// Get a version first
const version = await api.getLatestCliVersion('stable');
const version = await api.getLatestCliVersion(defaultCLIReleaseChannel);
const platform: CliSupportedPlatform = 'linux';

const checksum = await api.getSha256Checksum(version, platform);
Expand All @@ -108,7 +155,7 @@ suite('StaticCliApi - Integration Tests', function () {
});

test('getSha256Checksum handles version with and without v prefix', async () => {
const version = await api.getLatestCliVersion('stable');
const version = await api.getLatestCliVersion(defaultCLIReleaseChannel);
const platform: CliSupportedPlatform = 'linux';

// Remove 'v' prefix if present for testing
Expand All @@ -125,7 +172,7 @@ suite('StaticCliApi - Integration Tests', function () {
const platform: CliSupportedPlatform = 'linux';

// Get latest version
const version = await api.getLatestCliVersion('stable');
const version = await api.getLatestCliVersion(defaultCLIReleaseChannel);
console.log(`Testing with CLI version: ${version}`);

// Get the expected checksum
Expand Down Expand Up @@ -183,7 +230,7 @@ suite('StaticCliApi - Integration Tests', function () {

// This should throw an error
try {
await invalidApi.getLatestCliVersion('stable');
await invalidApi.getLatestCliVersion(defaultCLIReleaseChannel);
throw new Error('Expected getLatestCliVersion to throw an error');
} catch (err) {
// Accept any error since we're using an invalid domain
Expand Down
Loading