Skip to content

Commit c030821

Browse files
authored
Merge branch 'main' into dev/rigibson/integration-test-restore
2 parents a56489b + 29c1583 commit c030821

File tree

7 files changed

+229
-20
lines changed

7 files changed

+229
-20
lines changed

.github/workflows/branch-snap.yml

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
name: Branch snap
22
on:
3-
workflow_dispatch
3+
workflow_dispatch:
4+
inputs:
5+
releaseCandidate:
6+
description: 'Is this a release candidate snap from main? (Will increment main version to be higher than next stable release)'
7+
required: false
8+
default: 'false'
9+
type: boolean
410

511
permissions:
612
contents: write
@@ -18,19 +24,50 @@ jobs:
1824
runs-on: ubuntu-latest
1925
steps:
2026
- name: Check out
21-
uses: actions/checkout@v2
27+
uses: actions/checkout@v4
2228
- name: Install NodeJS
2329
uses: actions/setup-node@v4
2430
with:
2531
node-version: '18.x'
2632
- name: Install dependencies
2733
run: npm ci
2834
- name: Update version.json
29-
run: npx gulp incrementVersion
35+
run: |
36+
if [ "${{ github.event.inputs.releaseCandidate }}" = "true" ]; then
37+
npx gulp incrementVersion --releaseCandidate=true
38+
else
39+
npx gulp incrementVersion
40+
fi
3041
- name: Create version update PR
3142
uses: peter-evans/create-pull-request@v4
3243
with:
3344
token: ${{ secrets.GITHUB_TOKEN }}
3445
commit-message: Update main version
3546
title: '[automated] Update main version'
3647
branch: merge/update-main-version
48+
49+
update-release-version:
50+
needs: check-script
51+
if: github.ref == 'refs/heads/prerelease'
52+
runs-on: ubuntu-latest
53+
steps:
54+
- name: Check out merge branch
55+
uses: actions/checkout@v4
56+
with:
57+
ref: merge/prerelease-to-release
58+
fetch-depth: 0
59+
- name: Install NodeJS
60+
uses: actions/setup-node@v4
61+
with:
62+
node-version: '18.x'
63+
- name: Install dependencies
64+
run: npm ci
65+
- name: Update version.json for release
66+
run: npx gulp updateVersionForStableRelease
67+
- name: Create PR with version update
68+
uses: peter-evans/create-pull-request@v4
69+
with:
70+
token: ${{ secrets.GITHUB_TOKEN }}
71+
commit-message: Update version for stable release
72+
branch: merge/prerelease-to-release
73+
base: release

CONTRIBUTING.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,15 +196,25 @@ More details for this are [here] (https://devdiv.visualstudio.com/DevDiv/_git/Vi
196196
## Snapping for releases
197197
Extension releases on the marketplace are done from the prerelease and release branches (corresponding to the prerelease or release version of the extension). Code flows from main -> prerelease -> release. Every week we snap main -> prerelease. Monthly, we snap prerelease -> release.
198198

199+
### Versioning Scheme
200+
The extension follows a specific versioning scheme for releases:
201+
- **Prerelease versions**: Use standard minor version increments (e.g., 2.74, 2.75, 2.76...)
202+
- **Stable release versions**: Use the next tens version (e.g., 2.74 prerelease becomes 2.80 stable)
203+
- **Main branch after RC snap**: Jumps to one above the next stable version (e.g., if snapping 2.74 as RC, main becomes 2.81)
204+
199205
### Snap main -> prerelease
200206
The snap is done via the "Branch snap" github action. To run the snap from main -> prerelease, run the action via "Run workflow" and choose main as the base branch.
201207
![branch snap action](./docs/images/main_snap.png)
202208

209+
When running the snap action, you can optionally check the "Is this a release candidate snap" checkbox. If checked:
210+
- The prerelease branch will receive the snapped code with the current version (e.g., 2.74)
211+
- The main branch version will be updated to be higher than the next stable release (e.g., 2.81, since the next stable would be 2.80)
212+
203213
This will generate two PRs that must be merged. One merging the main branch into prerelease, and the other bumps the version in main.
204214
![generated prs](./docs/images/generated_prs.png)
205215

206216
### Snap prerelease -> release
207-
To snap from prerelease to release, run the same action but use **prerelease** as the workflow branch. This will generate a single PR merging from prerelease to release.
217+
To snap from prerelease to release, run the same action but use **prerelease** as the workflow branch. This will generate a PR merging from prerelease to release, and automatically update the version to the next stable release version (e.g., 2.74 -> 2.80) on the merge branch before the PR is merged.
208218

209219
### Marketplace release
210220
The marketplace release is managed by an internal AzDo pipeline. On the pipeline page, hit run pipeline. This will bring up the pipeline parameters to fill out:

jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const config: Config = {
1414
'<rootDir>/test/razor/razorIntegrationTests/jest.config.ts',
1515
'<rootDir>/test/razor/razorTests/jest.config.ts',
1616
'<rootDir>/test/untrustedWorkspace/integrationTests/jest.config.ts',
17+
'<rootDir>/test/tasks/jest.config.ts',
1718
],
1819
// Reporters are a global jest configuration property and cannot be set in the project jest config.
1920
// This configuration will create a 'junit.xml' file in the output directory, no matter which test project is running.

tasks/snapTasks.ts

Lines changed: 92 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as os from 'os';
1010
import { exec } from 'child_process';
1111
import { promisify } from 'util';
1212
import { findTagsByVersion } from './gitTasks';
13+
import minimist from 'minimist';
1314

1415
const execAsync = promisify(exec);
1516

@@ -20,27 +21,50 @@ function logWarning(message: string, error?: unknown): void {
2021
}
2122
}
2223

23-
gulp.task('incrementVersion', async (): Promise<void> => {
24-
// Get the current version from version.json
24+
/**
25+
* Calculate the next release (stable) version from the current version.
26+
* Rounds up the minor version to the next tens version.
27+
* @param currentVersion The current version in "major.minor" format (e.g., "2.74")
28+
* @returns The next stable release version (e.g., "2.80")
29+
*/
30+
export function getNextReleaseVersion(currentVersion: string): string {
31+
const split = currentVersion.split('.');
32+
const major = parseInt(split[0]);
33+
const minor = parseInt(split[1]);
34+
35+
// Round up to the next tens version
36+
const nextTensMinor = Math.ceil((minor + 1) / 10) * 10;
37+
38+
return `${major}.${nextTensMinor}`;
39+
}
40+
41+
/**
42+
* Read and parse version.json
43+
* @returns The parsed version.json object
44+
*/
45+
function readVersionJson(): { version: string; [key: string]: unknown } {
2546
const versionFilePath = path.join(path.resolve(__dirname, '..'), 'version.json');
2647
const file = fs.readFileSync(versionFilePath, 'utf8');
27-
const versionJson = JSON.parse(file);
28-
29-
// Increment the minor version
30-
const version = versionJson.version as string;
31-
const split = version.split('.');
32-
const newVersion = `${split[0]}.${parseInt(split[1]) + 1}`;
33-
34-
console.log(`Updating ${version} to ${newVersion}`);
48+
return JSON.parse(file);
49+
}
3550

36-
// Write the new version back to version.json
37-
versionJson.version = newVersion;
51+
/**
52+
* Write version.json with the given version
53+
* @param versionJson The version.json object to write
54+
*/
55+
function writeVersionJson(versionJson: { version: string; [key: string]: unknown }): void {
56+
const versionFilePath = path.join(path.resolve(__dirname, '..'), 'version.json');
3857
const newJson = JSON.stringify(versionJson, null, 4);
3958
console.log(`New json: ${newJson}`);
40-
4159
fs.writeFileSync(versionFilePath, newJson);
60+
}
4261

43-
// Add a new changelog section for the new version.
62+
/**
63+
* Add a new version section to the changelog
64+
* @param version The version to add (e.g., "2.75")
65+
* @param additionalLines Optional additional lines to add after the version header
66+
*/
67+
function addChangelogSection(version: string, additionalLines?: string[]): void {
4468
console.log('Adding new version header to changelog');
4569

4670
const changelogPath = path.join(path.resolve(__dirname, '..'), 'CHANGELOG.md');
@@ -71,10 +95,41 @@ gulp.task('incrementVersion', async (): Promise<void> => {
7195
// Insert a new header for the new version after the known issues header but before the next header.
7296
const lineToInsertAt = matches[knownIssuesIndex + 1].line - 1;
7397
console.log(`Inserting new version header at line ${lineToInsertAt}`);
74-
const linesToInsert = ['', `# ${newVersion}.x`];
98+
const linesToInsert = ['', `# ${version}.x`];
99+
100+
// Add any additional lines if provided
101+
if (additionalLines && additionalLines.length > 0) {
102+
linesToInsert.push(...additionalLines);
103+
}
75104

76105
changelogLines.splice(lineToInsertAt, 0, ...linesToInsert);
77106
fs.writeFileSync(changelogPath, changelogLines.join(os.EOL));
107+
}
108+
109+
gulp.task('incrementVersion', async (): Promise<void> => {
110+
const argv = minimist(process.argv.slice(2));
111+
const isReleaseCandidate = argv['releaseCandidate'] === true || argv['releaseCandidate'] === 'true';
112+
113+
// Get the current version from version.json
114+
const versionJson = readVersionJson();
115+
116+
// Calculate new version
117+
let version = versionJson.version as string;
118+
if (isReleaseCandidate) {
119+
version = getNextReleaseVersion(version);
120+
console.log(`Release candidate, using base version of ${version}`);
121+
}
122+
123+
const split = version.split('.');
124+
const newVersion = `${split[0]}.${parseInt(split[1]) + 1}`;
125+
console.log(`Updating ${versionJson.version} to ${newVersion}`);
126+
127+
// Write the new version back to version.json
128+
versionJson.version = newVersion;
129+
writeVersionJson(versionJson);
130+
131+
// Add a new changelog section for the new version.
132+
addChangelogSection(newVersion);
78133
});
79134

80135
gulp.task('updateChangelog', async (): Promise<void> => {
@@ -186,3 +241,25 @@ async function generatePRList(startSHA: string, endSHA: string): Promise<string[
186241
throw error;
187242
}
188243
}
244+
245+
/**
246+
* Update version.json to the next stable release version.
247+
* This task is used when snapping from prerelease to release.
248+
* It updates the version to round up to the next tens version (e.g., 2.74 -> 2.80).
249+
*/
250+
gulp.task('updateVersionForStableRelease', async (): Promise<void> => {
251+
// Get the current version from version.json
252+
const versionJson = readVersionJson();
253+
254+
const currentVersion = versionJson.version as string;
255+
const releaseVersion = getNextReleaseVersion(currentVersion);
256+
257+
console.log(`Updating version from ${currentVersion} to stable release version ${releaseVersion}`);
258+
259+
// Write the new version back to version.json
260+
versionJson.version = releaseVersion;
261+
writeVersionJson(versionJson);
262+
263+
// Add a new changelog section for the release version that references the prerelease
264+
addChangelogSection(releaseVersion, [`* See ${currentVersion}.x for full list of changes.`]);
265+
});

tasks/testTasks.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { jestOmniSharpUnitTestProjectName } from '../test/omnisharp/omnisharpUni
1212
import { jestUnitTestProjectName } from '../test/lsptoolshost/unitTests/jest.config';
1313
import { razorTestProjectName } from '../test/razor/razorTests/jest.config';
1414
import { jestArtifactTestsProjectName } from '../test/lsptoolshost/artifactTests/jest.config';
15+
import { jestTasksTestProjectName } from '../test/tasks/jest.config';
1516
import {
1617
getJUnitFileName,
1718
integrationTestProjects,
@@ -46,7 +47,11 @@ function createUnitTestSubTasks() {
4647
await runJestTest(razorTestProjectName);
4748
});
4849

49-
gulp.task('test:unit', gulp.series('test:unit:csharp', 'test:unit:razor'));
50+
gulp.task('test:unit:tasks', async () => {
51+
await runJestTest(jestTasksTestProjectName);
52+
});
53+
54+
gulp.task('test:unit', gulp.series('test:unit:csharp', 'test:unit:razor', 'test:unit:tasks'));
5055
}
5156

5257
function createIntegrationTestSubTasks() {

test/tasks/jest.config.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import type { Config } from 'jest';
6+
import { baseProjectConfig } from '../../baseJestConfig';
7+
8+
export const jestTasksTestProjectName = 'Tasks Unit Tests';
9+
10+
/**
11+
* Defines a jest project configuration for tasks unit tests.
12+
*/
13+
const tasksTestConfig: Config = {
14+
...baseProjectConfig,
15+
displayName: jestTasksTestProjectName,
16+
modulePathIgnorePatterns: ['out'],
17+
roots: ['<rootDir>', '<rootDir>../../__mocks__'],
18+
};
19+
20+
export default tasksTestConfig;

test/tasks/versionHelper.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { getNextReleaseVersion } from '../../tasks/snapTasks';
7+
import { describe, test, expect } from '@jest/globals';
8+
9+
describe('getNextReleaseVersion', () => {
10+
test('rounds up to next tens from single digit minor version', () => {
11+
expect(getNextReleaseVersion('2.1')).toBe('2.10');
12+
expect(getNextReleaseVersion('2.5')).toBe('2.10');
13+
expect(getNextReleaseVersion('2.9')).toBe('2.10');
14+
});
15+
16+
test('rounds up to next tens from teens minor version', () => {
17+
expect(getNextReleaseVersion('2.10')).toBe('2.20');
18+
expect(getNextReleaseVersion('2.15')).toBe('2.20');
19+
expect(getNextReleaseVersion('2.19')).toBe('2.20');
20+
});
21+
22+
test('rounds up to next tens from twenties minor version', () => {
23+
expect(getNextReleaseVersion('2.20')).toBe('2.30');
24+
expect(getNextReleaseVersion('2.25')).toBe('2.30');
25+
expect(getNextReleaseVersion('2.29')).toBe('2.30');
26+
});
27+
28+
test('rounds up from 74 to 80', () => {
29+
expect(getNextReleaseVersion('2.74')).toBe('2.80');
30+
});
31+
32+
test('rounds up from 75 to 80', () => {
33+
expect(getNextReleaseVersion('2.75')).toBe('2.80');
34+
});
35+
36+
test('rounds up from 79 to 80', () => {
37+
expect(getNextReleaseVersion('2.79')).toBe('2.80');
38+
});
39+
40+
test('rounds up from 80 to 90', () => {
41+
expect(getNextReleaseVersion('2.80')).toBe('2.90');
42+
});
43+
44+
test('rounds up from 90 to 100', () => {
45+
expect(getNextReleaseVersion('2.90')).toBe('2.100');
46+
});
47+
48+
test('works with different major versions', () => {
49+
expect(getNextReleaseVersion('1.74')).toBe('1.80');
50+
expect(getNextReleaseVersion('3.55')).toBe('3.60');
51+
expect(getNextReleaseVersion('10.99')).toBe('10.100');
52+
});
53+
54+
test('handles version at exactly tens boundary', () => {
55+
expect(getNextReleaseVersion('2.10')).toBe('2.20');
56+
expect(getNextReleaseVersion('2.20')).toBe('2.30');
57+
expect(getNextReleaseVersion('2.30')).toBe('2.40');
58+
});
59+
});

0 commit comments

Comments
 (0)