Skip to content

Commit d6cbe35

Browse files
committed
Updates ci-cd commands to set node version based on SPFx project version. Closes #6717
1 parent 24af61c commit d6cbe35

File tree

9 files changed

+854
-618
lines changed

9 files changed

+854
-618
lines changed

src/m365/spfx/commands/SpfxCompatibilityMatrix.ts

Lines changed: 611 additions & 0 deletions
Large diffs are not rendered by default.

src/m365/spfx/commands/project/DeployWorkflow.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const workflow: GitHubWorkflow = {
1414
"build-and-deploy": {
1515
"runs-on": "ubuntu-latest",
1616
env: {
17-
NodeVersion: "22.x"
17+
NodeVersion: ""
1818
},
1919
steps: [
2020
{
@@ -115,7 +115,7 @@ export const pipeline: AzureDevOpsPipeline = {
115115
},
116116
{
117117
name: "NodeVersion",
118-
value: "22.x"
118+
value: ""
119119
}
120120
],
121121
stages: [

src/m365/spfx/commands/project/project-azuredevops-pipeline-add.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CommandInfo } from '../../../../cli/CommandInfo.js';
88
import { Logger } from '../../../../cli/Logger.js';
99
import { telemetry } from '../../../../telemetry.js';
1010
import { pid } from '../../../../utils/pid.js';
11+
import { spfx } from '../../../../utils/spfx.js';
1112
import { session } from '../../../../utils/session.js';
1213
import { sinonUtil } from '../../../../utils/sinonUtil.js';
1314
import commands from '../../commands.js';
@@ -22,6 +23,7 @@ describe(commands.PROJECT_AZUREDEVOPS_PIPELINE_ADD, () => {
2223
before(() => {
2324
sinon.stub(telemetry, 'trackEvent').resolves();
2425
sinon.stub(pid, 'getProcessName').callsFake(() => '');
26+
sinon.stub(spfx, 'getHighestNodeVersion').callsFake(() => '22.0.x');
2527
sinon.stub(session, 'getId').callsFake(() => '');
2628
commandInfo = cli.getCommandInfo(command);
2729
});
@@ -44,6 +46,7 @@ describe(commands.PROJECT_AZUREDEVOPS_PIPELINE_ADD, () => {
4446
afterEach(() => {
4547
sinonUtil.restore([
4648
(command as any).getProjectRoot,
49+
(command as any).getProjectVersion,
4750
fs.existsSync,
4851
fs.readFileSync,
4952
fs.writeFileSync
@@ -89,6 +92,8 @@ describe(commands.PROJECT_AZUREDEVOPS_PIPELINE_ADD, () => {
8992
return '';
9093
});
9194

95+
sinon.stub(command as any, 'getProjectVersion').returns('1.16.0');
96+
9297
const writeFileSyncStub: sinon.SinonStub = sinon.stub(fs, 'writeFileSync').resolves({});
9398

9499
await command.action(logger, { options: { name: 'test', branchName: 'dev', skipFeatureDeployment: true, loginMethod: 'user', scope: 'sitecollection', siteUrl: 'https://contoso.sharepoint.com/sites/project' } } as any);
@@ -148,12 +153,74 @@ describe(commands.PROJECT_AZUREDEVOPS_PIPELINE_ADD, () => {
148153
return '';
149154
});
150155

156+
sinon.stub(command as any, 'getProjectVersion').returns('1.21.1');
157+
151158
const writeFileSyncStub: sinon.SinonStub = sinon.stub(fs, 'writeFileSync').resolves({});
152159

153160
await command.action(logger, { options: { debug: true } } as any);
154161
assert(writeFileSyncStub.calledWith(path.join(process.cwd(), projectPath, '/.azuredevops', 'pipelines', 'deploy-spfx-solution.yml')), 'workflow file not created');
155162
});
156163

164+
it('handles error with unknown minor version of SPFx when missing minor version', async () => {
165+
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));
166+
167+
sinon.stub(fs, 'readFileSync').callsFake((path, options) => {
168+
if (path.toString().endsWith('package.json') && options === 'utf-8') {
169+
return '{"name": "test"}';
170+
}
171+
172+
return '';
173+
});
174+
175+
sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
176+
if (fakePath.toString().endsWith('.azuredevops')) {
177+
return true;
178+
}
179+
else if (fakePath.toString().endsWith('pipelines')) {
180+
return true;
181+
}
182+
183+
return false;
184+
});
185+
186+
sinon.stub(command as any, 'getProjectVersion').returns('');
187+
188+
sinon.stub(fs, 'writeFileSync').callsFake(() => { throw 'error'; });
189+
190+
await assert.rejects(command.action(logger, { options: {} } as any),
191+
new CommandError(`Unable to determine the version of the current SharePoint Framework project`, undefined));
192+
});
193+
194+
it('handles error with not found node version', async () => {
195+
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));
196+
197+
sinon.stub(fs, 'readFileSync').callsFake((path, options) => {
198+
if (path.toString().endsWith('package.json') && options === 'utf-8') {
199+
return '{"name": "test"}';
200+
}
201+
202+
return '';
203+
});
204+
205+
sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
206+
if (fakePath.toString().endsWith('.azuredevops')) {
207+
return true;
208+
}
209+
else if (fakePath.toString().endsWith('pipelines')) {
210+
return true;
211+
}
212+
213+
return false;
214+
});
215+
216+
sinon.stub(command as any, 'getProjectVersion').returns('99.99.99');
217+
218+
sinon.stub(fs, 'writeFileSync').callsFake(() => { throw 'error'; });
219+
220+
await assert.rejects(command.action(logger, { options: {} } as any),
221+
new CommandError(`Could not find Node version for 99.99.99 of SharePoint Framework`, undefined));
222+
});
223+
157224
it('handles unexpected error', async () => {
158225
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));
159226

@@ -176,6 +243,8 @@ describe(commands.PROJECT_AZUREDEVOPS_PIPELINE_ADD, () => {
176243
return false;
177244
});
178245

246+
sinon.stub(command as any, 'getProjectVersion').returns('1.21.1');
247+
179248
sinon.stub(fs, 'writeFileSync').callsFake(() => { throw 'error'; });
180249

181250
await assert.rejects(command.action(logger, { options: {} } as any),

src/m365/spfx/commands/project/project-azuredevops-pipeline-add.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { pipeline } from './DeployWorkflow.js';
1010
import { fsUtil } from '../../../../utils/fsUtil.js';
1111
import { AzureDevOpsPipeline, AzureDevOpsPipelineStep } from './project-azuredevops-pipeline-model.js';
1212
import GlobalOptions from '../../../../GlobalOptions.js';
13+
import { versions } from '../SpfxCompatibilityMatrix.js';
14+
import { spfx } from '../../../../utils/spfx.js';
1315

1416
interface CommandArgs {
1517
options: Options;
@@ -155,6 +157,24 @@ class SpfxProjectAzureDevOpsPipelineAddCommand extends BaseProjectCommand {
155157
pipeline.trigger.branches.include[0] = options.branchName;
156158
}
157159

160+
const version = this.getProjectVersion();
161+
162+
if (!version) {
163+
throw 'Unable to determine the version of the current SharePoint Framework project';
164+
}
165+
166+
const versionRequirements = versions[version];
167+
168+
if (!versionRequirements) {
169+
throw `Could not find Node version for ${version} of SharePoint Framework`;
170+
}
171+
172+
const nodeVersion: string = spfx.getHighestNodeVersion(versionRequirements.node.range);
173+
174+
if (nodeVersion) {
175+
this.assignPipelineVariables(pipeline, 'NodeVersion', nodeVersion);
176+
}
177+
158178
const script = this.getScriptAction(pipeline);
159179
if (script.script) {
160180
if (options.loginMethod === 'user') {

src/m365/spfx/commands/project/project-github-workflow-add.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CommandInfo } from '../../../../cli/CommandInfo.js';
88
import { Logger } from '../../../../cli/Logger.js';
99
import { telemetry } from '../../../../telemetry.js';
1010
import { pid } from '../../../../utils/pid.js';
11+
import { spfx } from '../../../../utils/spfx.js';
1112
import { session } from '../../../../utils/session.js';
1213
import { sinonUtil } from '../../../../utils/sinonUtil.js';
1314
import commands from '../../commands.js';
@@ -22,6 +23,7 @@ describe(commands.PROJECT_GITHUB_WORKFLOW_ADD, () => {
2223
before(() => {
2324
sinon.stub(telemetry, 'trackEvent').resolves();
2425
sinon.stub(pid, 'getProcessName').callsFake(() => '');
26+
sinon.stub(spfx, 'getHighestNodeVersion').callsFake(() => '22.0.x');
2527
sinon.stub(session, 'getId').callsFake(() => '');
2628
commandInfo = cli.getCommandInfo(command);
2729
});
@@ -44,6 +46,7 @@ describe(commands.PROJECT_GITHUB_WORKFLOW_ADD, () => {
4446
afterEach(() => {
4547
sinonUtil.restore([
4648
(command as any).getProjectRoot,
49+
(command as any).getProjectVersion,
4750
fs.existsSync,
4851
fs.readFileSync,
4952
fs.writeFileSync
@@ -116,6 +119,8 @@ describe(commands.PROJECT_GITHUB_WORKFLOW_ADD, () => {
116119
return '';
117120
});
118121

122+
sinon.stub(command as any, 'getProjectVersion').returns('1.21.1');
123+
119124
const writeFileSyncStub: sinon.SinonStub = sinon.stub(fs, 'writeFileSync').resolves({});
120125

121126
await command.action(logger, { options: { debug: true } } as any);
@@ -149,12 +154,74 @@ describe(commands.PROJECT_GITHUB_WORKFLOW_ADD, () => {
149154
return '';
150155
});
151156

157+
sinon.stub(command as any, 'getProjectVersion').returns('1.21.1');
158+
152159
const writeFileSyncStub: sinon.SinonStub = sinon.stub(fs, 'writeFileSync').resolves({});
153160

154161
await command.action(logger, { options: { name: 'test', branchName: 'dev', manuallyTrigger: true, skipFeatureDeployment: true, loginMethod: 'user', scope: 'sitecollection' } } as any);
155162
assert(writeFileSyncStub.calledWith(path.join(process.cwd(), projectPath, '/.github', 'workflows', 'deploy-spfx-solution.yml')), 'workflow file not created');
156163
});
157164

165+
it('handles error with unknown version of SPFx', async () => {
166+
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));
167+
168+
sinon.stub(fs, 'readFileSync').callsFake((path, options) => {
169+
if (path.toString().endsWith('package.json') && options === 'utf-8') {
170+
return '{"name": "test"}';
171+
}
172+
173+
return '';
174+
});
175+
176+
sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
177+
if (fakePath.toString().endsWith('.github')) {
178+
return true;
179+
}
180+
else if (fakePath.toString().endsWith('workflows')) {
181+
return true;
182+
}
183+
184+
return false;
185+
});
186+
187+
sinon.stub(command as any, 'getProjectVersion').returns(undefined);
188+
189+
sinon.stub(fs, 'writeFileSync').callsFake(() => { throw 'error'; });
190+
191+
await assert.rejects(command.action(logger, { options: {} } as any),
192+
new CommandError(`Unable to determine the version of the current SharePoint Framework project`, undefined));
193+
});
194+
195+
it('handles error with not found node version', async () => {
196+
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));
197+
198+
sinon.stub(fs, 'readFileSync').callsFake((path, options) => {
199+
if (path.toString().endsWith('package.json') && options === 'utf-8') {
200+
return '{"name": "test"}';
201+
}
202+
203+
return '';
204+
});
205+
206+
sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
207+
if (fakePath.toString().endsWith('.github')) {
208+
return true;
209+
}
210+
else if (fakePath.toString().endsWith('workflows')) {
211+
return true;
212+
}
213+
214+
return false;
215+
});
216+
217+
sinon.stub(command as any, 'getProjectVersion').returns('99.99.99');
218+
219+
sinon.stub(fs, 'writeFileSync').callsFake(() => { throw 'error'; });
220+
221+
await assert.rejects(command.action(logger, { options: {} } as any),
222+
new CommandError(`Could not find Node version for 99.99.99 of SharePoint Framework`, undefined));
223+
});
224+
158225
it('handles unexpected error', async () => {
159226
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));
160227

@@ -177,6 +244,8 @@ describe(commands.PROJECT_GITHUB_WORKFLOW_ADD, () => {
177244
return false;
178245
});
179246

247+
sinon.stub(command as any, 'getProjectVersion').returns('1.21.1');
248+
180249
sinon.stub(fs, 'writeFileSync').callsFake(() => { throw 'error'; });
181250

182251
await assert.rejects(command.action(logger, { options: {} } as any),

src/m365/spfx/commands/project/project-github-workflow-add.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import commands from '../../commands.js';
1010
import { workflow } from './DeployWorkflow.js';
1111
import { BaseProjectCommand } from './base-project-command.js';
1212
import { GitHubWorkflow, GitHubWorkflowStep } from './project-github-workflow-model.js';
13+
import { versions } from '../SpfxCompatibilityMatrix.js';
14+
import { spfx } from '../../../../utils/spfx.js';
1315

1416
interface CommandArgs {
1517
options: Options;
@@ -155,6 +157,24 @@ class SpfxProjectGithubWorkflowAddCommand extends BaseProjectCommand {
155157
workflow.on.push.branches[0] = options.branchName;
156158
}
157159

160+
const version = this.getProjectVersion();
161+
162+
if (!version) {
163+
throw 'Unable to determine the version of the current SharePoint Framework project';
164+
}
165+
166+
const versionRequirements = versions[version];
167+
168+
if (!versionRequirements) {
169+
throw `Could not find Node version for ${version} of SharePoint Framework`;
170+
}
171+
172+
const nodeVersion: string = spfx.getHighestNodeVersion(versionRequirements.node.range);
173+
174+
if (nodeVersion) {
175+
this.assignNodeVersion(workflow, nodeVersion);
176+
}
177+
158178
if (options.manuallyTrigger) {
159179
// eslint-disable-next-line camelcase
160180
workflow.on.workflow_dispatch = null;
@@ -184,6 +204,10 @@ class SpfxProjectGithubWorkflowAddCommand extends BaseProjectCommand {
184204
}
185205
}
186206

207+
private assignNodeVersion(workflow: GitHubWorkflow, nodeVersion: string): void {
208+
workflow.jobs['build-and-deploy'].env.NodeVersion = nodeVersion;
209+
}
210+
187211
private getLoginAction(workflow: GitHubWorkflow): GitHubWorkflowStep {
188212
const steps = this.getWorkFlowSteps(workflow);
189213
return steps.find(step => step.uses && step.uses.indexOf('action-cli-login') >= 0)!;

0 commit comments

Comments
 (0)