Skip to content

Commit 8c1bfd7

Browse files
authored
ci(tools): add experimental release pipeline for tools packages (microsoft#35962)
1 parent 8a6b0b9 commit 8c1bfd7

7 files changed

Lines changed: 355 additions & 12 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
pr: none
2+
trigger: none
3+
4+
parameters:
5+
- name: dryRun
6+
displayName: Dry Run Mode
7+
type: boolean
8+
default: true
9+
10+
# Customize build number to include tools experimental prefix
11+
# Example: tools_experimental_20201022.1
12+
name: 'tools_experimental_$(Date:yyyyMMdd)$(Rev:.r)'
13+
14+
variables:
15+
- group: 'Github and NPM secrets'
16+
- template: .devops/templates/variables.yml
17+
parameters:
18+
skipComponentGovernanceDetection: false
19+
- name: tags
20+
value: production,externalfacing
21+
22+
resources:
23+
repositories:
24+
- repository: 1esPipelines
25+
type: git
26+
name: 1ESPipelineTemplates/1ESPipelineTemplates
27+
ref: refs/tags/release
28+
29+
extends:
30+
template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines
31+
parameters:
32+
pool:
33+
name: Azure-Pipelines-1ESPT-ExDShared
34+
image: windows-latest
35+
os: windows # We need windows because compliance task only run on windows.
36+
stages:
37+
- stage: main
38+
jobs:
39+
- job: Release
40+
pool:
41+
name: '1ES-Host-Ubuntu'
42+
image: '1ES-PT-Ubuntu-20.04'
43+
os: linux
44+
timeoutInMinutes: 90
45+
workspace:
46+
clean: all
47+
templateContext:
48+
outputParentDirectory: $(System.DefaultWorkingDirectory)
49+
outputs:
50+
- output: pipelineArtifact
51+
targetPath: $(System.DefaultWorkingDirectory)
52+
artifactName: output
53+
steps:
54+
- template: .devops/templates/tools.yml@self
55+
parameters:
56+
dryRun: ${{ parameters.dryRun }}
57+
58+
- script: |
59+
git config user.name "Fluent UI Build"
60+
git config user.email "fluentui-internal@service.microsoft.com"
61+
displayName: Configure git user (used by beachball)
62+
63+
- task: Bash@3
64+
name: validation
65+
inputs:
66+
targetType: 'inline'
67+
script: |
68+
BRANCH="$(Build.SourceBranch)"
69+
if [[ ! $BRANCH =~ refs/heads/experimental/ ]]; then
70+
echo "##vso[task.logissue type=error]Branch '$BRANCH' must start with 'refs/heads/experimental/'"
71+
exit 1
72+
fi
73+
BRANCH_PATH=${BRANCH#refs/heads/}
74+
FEATURE_NAME=${BRANCH#refs/heads/experimental/}
75+
echo "##vso[task.setvariable variable=branchPath;isOutput=true]$BRANCH_PATH"
76+
echo "##vso[task.setvariable variable=featureName;isOutput=true]$FEATURE_NAME"
77+
echo "Branch path: $BRANCH_PATH"
78+
echo "Feature name: $FEATURE_NAME"
79+
displayName: Validate branch and extract feature name
80+
81+
- script: |
82+
yarn install --frozen-lockfile
83+
displayName: Install dependencies
84+
85+
# Deletes all existing changefiles so that only bump that happens is for experimental
86+
- script: |
87+
rm -f change/*
88+
displayName: 'Delete existing changefiles'
89+
90+
# Bumps all tools packages to x.x.x-experimental.<feature>.<date>-<hash> version
91+
# x.x.x is each package's own current version
92+
- script: |
93+
FEATURE_NAME=$(validation.featureName)
94+
DATE=$(date +"%Y%m%d")
95+
HASH=$(git rev-parse --short HEAD)
96+
VERSION_SUFFIX="experimental.${FEATURE_NAME}.${DATE}-${HASH}"
97+
98+
echo "Version suffix: ${VERSION_SUFFIX}"
99+
100+
yarn nx g @fluentui/workspace-plugin:version-bump --all --scope tools --versionSuffix "${VERSION_SUFFIX}"
101+
git add .
102+
git commit -m "bump tools experimental versions with suffix ${VERSION_SUFFIX}"
103+
yarn change --type prerelease --message "Release experimental tools ${VERSION_SUFFIX}" --dependent-change-type "prerelease"
104+
displayName: 'Bump and commit experimental versions'
105+
106+
- script: |
107+
FLUENT_PROD_BUILD=true yarn nx run-many -t build -p 'tag:tools,!tag:npm:private,!tag:v8' --exclude 'apps/**' --nxBail
108+
displayName: build
109+
110+
- script: |
111+
FLUENT_PROD_BUILD=true yarn nx run-many -t lint -p 'tag:tools,!tag:npm:private,!tag:v8' --exclude 'apps/**' --nxBail
112+
displayName: lint
113+
114+
- script: |
115+
FLUENT_PROD_BUILD=true yarn nx run-many -t test -p 'tag:tools,!tag:npm:private,!tag:v8' --exclude 'apps/**' --nxBail
116+
displayName: test
117+
118+
- script: |
119+
yarn beachball publish -b origin/$(validation.branchPath) --access public -y -n $(npmToken) --no-push --tag experimental --config scripts/beachball/src/release-tools.config.js
120+
git reset --hard origin/$(validation.branchPath)
121+
displayName: Publish changes and bump versions
122+
condition: and(succeeded(), not(${{ parameters.dryRun }}))
123+
124+
- template: .devops/templates/cleanup.yml@self
125+
parameters:
126+
checkForModifiedFiles: false

tools/workspace-plugin/src/generators/version-bump/README.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ The generator also bumps the versions in any dependent packages.
2626
- [`exclude`](#exclude)
2727
- [`bumpType`](#bumpType)
2828
- [`prereleaseTag`](#prereleaseTag)
29+
- [`scope`](#scope)
30+
- [`versionSuffix`](#versionsuffix)
2931

3032
<!-- tocstop -->
3133

3234
## NOTES
3335

34-
- Can bump single package or all convered packages
36+
- Can bump single package or all packages in a given scope
3537
- Bumps the package version in all dependent packages
3638
- Converged packages are currently only identified as having version `9.x`
3739

@@ -73,6 +75,12 @@ Bump all vNext packages for a nightly release (0.0.0-nightly).
7375
yarn nx g @fluentui/workspace-plugin:version-bump --all --bumpType nightly --prereleaseTag nightly
7476
```
7577

78+
Bump all tools packages for an experimental release, preserving each package's base version:
79+
80+
```sh
81+
yarn nx g @fluentui/workspace-plugin:version-bump --all --scope tools --versionSuffix "experimental.my-feature.20260408-abc1234"
82+
```
83+
7684
## Options
7785

7886
#### `name`
@@ -87,7 +95,7 @@ Project name (without @npmScope prefix - e.g. `<project-name>`)
8795

8896
Type: `boolean`
8997

90-
Run batch migration on all vNext packages with the tag `platform:web` in `nx.json`
98+
Run batch migration on all packages in the specified scope (see [`scope`](#scope)).
9199

92100
#### `exclude`
93101

@@ -114,3 +122,23 @@ Bump type that can be any allowed in the official NPM [semver](https://github.co
114122
Type: `string`
115123

116124
For example `alpha` or `beta` Only used when bumping prerelease versions.
125+
126+
### `scope`
127+
128+
Type: `string` (enum: `vNext`, `tools`)
129+
Default: `vNext`
130+
131+
Which package scope `--all` targets:
132+
133+
- `vNext` — all converged/vNext packages (default, backward compatible)
134+
- `tools` — all public tools packages (tagged `tools`, non-private, non-v8)
135+
136+
### `versionSuffix`
137+
138+
Type: `string`
139+
140+
A suffix to append to each package's current version. The resulting version will be `{currentVersion}-{versionSuffix}`.
141+
142+
- Mutually exclusive with `--bumpType` and `--explicitVersion`
143+
- Requires `--all`
144+
- Useful for experimental releases where each package keeps its own base version

tools/workspace-plugin/src/generators/version-bump/index.spec.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,126 @@ describe('version-string-replace generator', () => {
347347
`);
348348
});
349349
});
350+
351+
describe('--scope tools with --versionSuffix', () => {
352+
const suffix = 'experimental.my-feature.20260408-abc1234';
353+
354+
beforeEach(() => {
355+
// Tools packages
356+
tree = setupDummyPackage(tree, {
357+
name: 'react-storybook-addon',
358+
version: '0.6.0',
359+
dependencies: {},
360+
projectConfiguration: { tags: ['tools', 'platform:node'], sourceRoot: 'packages/react-storybook-addon/src' },
361+
});
362+
tree = setupDummyPackage(tree, {
363+
name: 'eslint-plugin-react-components',
364+
version: '0.2.1',
365+
dependencies: {
366+
'@proj/react-storybook-addon': '^0.6.0',
367+
},
368+
projectConfiguration: {
369+
tags: ['tools', 'platform:node'],
370+
sourceRoot: 'packages/eslint-plugin-react-components/src',
371+
},
372+
});
373+
// vNext package (should NOT be bumped)
374+
tree = setupDummyPackage(tree, {
375+
name: 'react-button',
376+
version: '9.0.0',
377+
dependencies: {},
378+
projectConfiguration: { tags: ['vNext', 'platform:web'], sourceRoot: 'packages/react-button/src' },
379+
});
380+
// Private tools package (should NOT be bumped)
381+
tree = setupDummyPackage(tree, {
382+
name: 'private-tool',
383+
version: '1.0.0',
384+
dependencies: {},
385+
projectConfiguration: { tags: ['tools', 'platform:node'], sourceRoot: 'packages/private-tool/src' },
386+
isPrivate: true,
387+
});
388+
});
389+
390+
it('should bump only tools packages with their own base version + suffix', async () => {
391+
await generator(tree, { all: true, scope: 'tools', versionSuffix: suffix });
392+
393+
const storybookAddon = readJson(tree, 'packages/react-storybook-addon/package.json');
394+
const eslintPlugin = readJson(tree, 'packages/eslint-plugin-react-components/package.json');
395+
const reactButton = readJson(tree, 'packages/react-button/package.json');
396+
397+
expect(storybookAddon.version).toBe(`0.6.0-${suffix}`);
398+
expect(eslintPlugin.version).toBe(`0.2.1-${suffix}`);
399+
// vNext package should not be affected
400+
expect(reactButton.version).toBe('9.0.0');
401+
});
402+
403+
it('should not bump private tools packages', async () => {
404+
await generator(tree, { all: true, scope: 'tools', versionSuffix: suffix });
405+
406+
const privateTool = readJson(tree, 'packages/private-tool/package.json');
407+
expect(privateTool.version).toBe('1.0.0');
408+
});
409+
410+
it('should update dependency versions for tools dependents', async () => {
411+
await generator(tree, { all: true, scope: 'tools', versionSuffix: suffix });
412+
413+
const eslintPlugin = readJson(tree, 'packages/eslint-plugin-react-components/package.json');
414+
expect(eslintPlugin.dependencies['@proj/react-storybook-addon']).toBe(`0.6.0-${suffix}`);
415+
});
416+
417+
it('should remove beachball disallowedChangeTypes for tools packages', async () => {
418+
tree = setupDummyPackage(tree, {
419+
name: 'react-conformance',
420+
version: '0.20.1',
421+
dependencies: {},
422+
projectConfiguration: { tags: ['tools', 'platform:node'], sourceRoot: 'packages/react-conformance/src' },
423+
beachball: {
424+
disallowedChangeTypes: ['prerelease'],
425+
},
426+
});
427+
428+
await generator(tree, { all: true, scope: 'tools', versionSuffix: suffix });
429+
430+
const reactConformance = readJson<PackageJsonWithBeachball>(tree, 'packages/react-conformance/package.json');
431+
expect(reactConformance.version).toBe(`0.20.1-${suffix}`);
432+
expect(reactConformance.beachball?.disallowedChangeTypes).toBeUndefined();
433+
});
434+
435+
it('should exclude specified tools packages', async () => {
436+
await generator(tree, {
437+
all: true,
438+
scope: 'tools',
439+
versionSuffix: suffix,
440+
exclude: 'react-storybook-addon',
441+
});
442+
443+
const storybookAddon = readJson(tree, 'packages/react-storybook-addon/package.json');
444+
const eslintPlugin = readJson(tree, 'packages/eslint-plugin-react-components/package.json');
445+
446+
expect(storybookAddon.version).toBe('0.6.0');
447+
expect(eslintPlugin.version).toBe(`0.2.1-${suffix}`);
448+
});
449+
});
450+
451+
describe('--versionSuffix validation', () => {
452+
it('should throw if --versionSuffix is used with --bumpType', async () => {
453+
await expect(
454+
generator(tree, { all: true, versionSuffix: 'experimental.feat.20260408-abc', bumpType: 'patch' }),
455+
).rejects.toThrow('--versionSuffix is mutually exclusive with --bumpType and --explicitVersion');
456+
});
457+
458+
it('should throw if --versionSuffix is used with --explicitVersion', async () => {
459+
await expect(
460+
generator(tree, { all: true, versionSuffix: 'experimental.feat.20260408-abc', explicitVersion: '1.0.0' }),
461+
).rejects.toThrow('--versionSuffix is mutually exclusive with --bumpType and --explicitVersion');
462+
});
463+
464+
it('should throw if --versionSuffix is used without --all', async () => {
465+
await expect(
466+
generator(tree, { name: 'react-button', versionSuffix: 'experimental.feat.20260408-abc' }),
467+
).rejects.toThrow('--versionSuffix requires --all');
468+
});
469+
});
350470
});
351471

352472
function setupDummyPackage(
@@ -358,6 +478,7 @@ function setupDummyPackage(
358478
dependencies: Record<string, string>;
359479
projectConfiguration: Partial<ReturnType<typeof readProjectConfiguration>>;
360480
beachball: PackageJsonWithBeachball['beachball'];
481+
isPrivate: boolean;
361482
}>,
362483
) {
363484
const workspaceConfig = getWorkspaceConfig(tree);
@@ -382,6 +503,7 @@ function setupDummyPackage(
382503
packageJson: {
383504
name: `@${workspaceConfig.npmScope}/${projectName}`,
384505
version: normalizedOptions.version,
506+
...(options.isPrivate ? { private: true } : {}),
385507
dependencies: normalizedOptions.dependencies,
386508
devDependencies: normalizedOptions.devDependencies,
387509
beachball: options.beachball,

0 commit comments

Comments
 (0)