Skip to content
Open
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
611 changes: 611 additions & 0 deletions src/m365/spfx/commands/SpfxCompatibilityMatrix.ts

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/m365/spfx/commands/project/DeployWorkflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const workflow: GitHubWorkflow = {
"build-and-deploy": {
"runs-on": "ubuntu-latest",
env: {
NodeVersion: "22.x"
NodeVersion: ""
},
steps: [
{
Expand Down Expand Up @@ -115,7 +115,7 @@ export const pipeline: AzureDevOpsPipeline = {
},
{
name: "NodeVersion",
value: "22.x"
value: ""
}
],
stages: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CommandInfo } from '../../../../cli/CommandInfo.js';
import { Logger } from '../../../../cli/Logger.js';
import { telemetry } from '../../../../telemetry.js';
import { pid } from '../../../../utils/pid.js';
import { spfx } from '../../../../utils/spfx.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
Expand All @@ -22,6 +23,7 @@ describe(commands.PROJECT_AZUREDEVOPS_PIPELINE_ADD, () => {
before(() => {
sinon.stub(telemetry, 'trackEvent').resolves();
sinon.stub(pid, 'getProcessName').callsFake(() => '');
sinon.stub(spfx, 'getHighestNodeVersion').returns('22.0.x');
sinon.stub(session, 'getId').callsFake(() => '');
commandInfo = cli.getCommandInfo(command);
});
Expand All @@ -44,6 +46,7 @@ describe(commands.PROJECT_AZUREDEVOPS_PIPELINE_ADD, () => {
afterEach(() => {
sinonUtil.restore([
(command as any).getProjectRoot,
(command as any).getProjectVersion,
fs.existsSync,
fs.readFileSync,
fs.writeFileSync
Expand Down Expand Up @@ -89,6 +92,8 @@ describe(commands.PROJECT_AZUREDEVOPS_PIPELINE_ADD, () => {
return '';
});

sinon.stub(command as any, 'getProjectVersion').returns('1.16.0');

const writeFileSyncStub: sinon.SinonStub = sinon.stub(fs, 'writeFileSync').resolves({});

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

sinon.stub(command as any, 'getProjectVersion').returns('1.21.1');

const writeFileSyncStub: sinon.SinonStub = sinon.stub(fs, 'writeFileSync').resolves({});

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

it('handles error with unknown minor version of SPFx when missing minor version', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));

sinon.stub(fs, 'readFileSync').callsFake((path, options) => {
if (path.toString().endsWith('package.json') && options === 'utf-8') {
return '{"name": "test"}';
}

return '';
});

sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
if (fakePath.toString().endsWith('.azuredevops')) {
return true;
}
else if (fakePath.toString().endsWith('pipelines')) {
return true;
}

return false;
});

sinon.stub(command as any, 'getProjectVersion').returns('');

sinon.stub(fs, 'writeFileSync').throws(new Error('writeFileSync failed'));

await assert.rejects(command.action(logger, { options: {} } as any),
new CommandError('Unable to determine the version of the current SharePoint Framework project. Could not find the correct version based on @microsoft/generator-sharepoint property in the .yo-rc.json file.'));
});

it('handles error with not found node version', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));

sinon.stub(fs, 'readFileSync').callsFake((path, options) => {
if (path.toString().endsWith('package.json') && options === 'utf-8') {
return '{"name": "test"}';
}

return '';
});

sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
if (fakePath.toString().endsWith('.azuredevops')) {
return true;
}
else if (fakePath.toString().endsWith('pipelines')) {
return true;
}

return false;
});

sinon.stub(command as any, 'getProjectVersion').returns('99.99.99');

sinon.stub(fs, 'writeFileSync').throws(new Error('writeFileSync failed'));

await assert.rejects(command.action(logger, { options: {} } as any),
new CommandError(`Could not find Node version for version '99.99.99' of SharePoint Framework.`));
});

it('handles unexpected error', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));

Expand All @@ -176,9 +243,43 @@ describe(commands.PROJECT_AZUREDEVOPS_PIPELINE_ADD, () => {
return false;
});

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

sinon.stub(fs, 'writeFileSync').throws(new Error('writeFileSync failed'));

await assert.rejects(command.action(logger, { options: {} } as any),
new CommandError('writeFileSync failed'));
});

it('handles unexpected non-error value', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));

sinon.stub(fs, 'readFileSync').callsFake((path, options) => {
if (path.toString().endsWith('package.json') && options === 'utf-8') {
return '{"name": "test"}';
}

return '';
});

sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
if (fakePath.toString().endsWith('.azuredevops')) {
return true;
}
else if (fakePath.toString().endsWith('pipelines')) {
return true;
}

return false;
});

sinon.stub(command as any, 'getProjectVersion').returns('1.21.1');

sinon.stub(fs, 'writeFileSync').callsFake(() => {
throw 'string failure';
});

await assert.rejects(command.action(logger, { options: {} } as any),
new CommandError('error'));
new CommandError('string failure'));
});
});
});
27 changes: 25 additions & 2 deletions src/m365/spfx/commands/project/project-azuredevops-pipeline-add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { pipeline } from './DeployWorkflow.js';
import { fsUtil } from '../../../../utils/fsUtil.js';
import { AzureDevOpsPipeline, AzureDevOpsPipelineStep } from './project-azuredevops-pipeline-model.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import { versions } from '../SpfxCompatibilityMatrix.js';
import { spfx } from '../../../../utils/spfx.js';

interface CommandArgs {
options: Options;
Expand Down Expand Up @@ -128,7 +130,12 @@ class SpfxProjectAzureDevOpsPipelineAddCommand extends BaseProjectCommand {
this.savePipeline(pipeline);
}
catch (error: any) {
throw new CommandError(error);
if (error instanceof CommandError) {
throw error;
}

const message = error instanceof Error ? error.message : String(error);
throw new CommandError(message);
}
}

Expand All @@ -155,6 +162,22 @@ class SpfxProjectAzureDevOpsPipelineAddCommand extends BaseProjectCommand {
pipeline.trigger.branches.include[0] = options.branchName;
}

const version = this.getProjectVersion();

if (!version) {
throw new CommandError('Unable to determine the version of the current SharePoint Framework project. Could not find the correct version based on @microsoft/generator-sharepoint property in the .yo-rc.json file.');
}

const versionRequirements = versions[version];

if (!versionRequirements) {
throw new CommandError(`Could not find Node version for version '${version}' of SharePoint Framework.`);
}

const nodeVersion: string = spfx.getHighestNodeVersion(versionRequirements.node.range);

this.assignPipelineVariables(pipeline, 'NodeVersion', nodeVersion);

const script = this.getScriptAction(pipeline);
if (script.script) {
if (options.loginMethod === 'user') {
Expand Down Expand Up @@ -213,4 +236,4 @@ class SpfxProjectAzureDevOpsPipelineAddCommand extends BaseProjectCommand {
}
}

export default new SpfxProjectAzureDevOpsPipelineAddCommand();
export default new SpfxProjectAzureDevOpsPipelineAddCommand();
110 changes: 107 additions & 3 deletions src/m365/spfx/commands/project/project-github-workflow-add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CommandInfo } from '../../../../cli/CommandInfo.js';
import { Logger } from '../../../../cli/Logger.js';
import { telemetry } from '../../../../telemetry.js';
import { pid } from '../../../../utils/pid.js';
import { spfx } from '../../../../utils/spfx.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
Expand All @@ -22,6 +23,7 @@ describe(commands.PROJECT_GITHUB_WORKFLOW_ADD, () => {
before(() => {
sinon.stub(telemetry, 'trackEvent').resolves();
sinon.stub(pid, 'getProcessName').callsFake(() => '');
sinon.stub(spfx, 'getHighestNodeVersion').returns('22.0.x');
sinon.stub(session, 'getId').callsFake(() => '');
commandInfo = cli.getCommandInfo(command);
});
Expand All @@ -44,6 +46,7 @@ describe(commands.PROJECT_GITHUB_WORKFLOW_ADD, () => {
afterEach(() => {
sinonUtil.restore([
(command as any).getProjectRoot,
(command as any).getProjectVersion,
fs.existsSync,
fs.readFileSync,
fs.writeFileSync
Expand Down Expand Up @@ -116,6 +119,8 @@ describe(commands.PROJECT_GITHUB_WORKFLOW_ADD, () => {
return '';
});

sinon.stub(command as any, 'getProjectVersion').returns('1.21.1');

const writeFileSyncStub: sinon.SinonStub = sinon.stub(fs, 'writeFileSync').resolves({});

await command.action(logger, { options: { debug: true } } as any);
Expand Down Expand Up @@ -149,12 +154,75 @@ describe(commands.PROJECT_GITHUB_WORKFLOW_ADD, () => {
return '';
});

sinon.stub(command as any, 'getProjectVersion').returns('1.21.1');

const writeFileSyncStub: sinon.SinonStub = sinon.stub(fs, 'writeFileSync').resolves({});

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

it('handles error with unknown version of SPFx', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));

sinon.stub(fs, 'readFileSync').callsFake((path, options) => {
if (path.toString().endsWith('package.json') && options === 'utf-8') {
return '{"name": "test"}';
}

return '';
});

sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
if (fakePath.toString().endsWith('.github')) {
return true;
}
else if (fakePath.toString().endsWith('workflows')) {
return true;
}

return false;
});

sinon.stub(command as any, 'getProjectVersion').returns(undefined);

sinon.stub(fs, 'writeFileSync').throws(new Error('writeFileSync failed'));

await assert.rejects(command.action(logger, { options: {} }),
new CommandError('Unable to determine the version of the current SharePoint Framework project. Could not find the correct version based on @microsoft/generator-sharepoint property in the .yo-rc.json file.'));

});

it('handles error with not found node version', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));

sinon.stub(fs, 'readFileSync').callsFake((path, options) => {
if (path.toString().endsWith('package.json') && options === 'utf-8') {
return '{"name": "test"}';
}

return '';
});

sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
if (fakePath.toString().endsWith('.github')) {
return true;
}
else if (fakePath.toString().endsWith('workflows')) {
return true;
}

return false;
});

sinon.stub(command as any, 'getProjectVersion').returns('99.99.99');

sinon.stub(fs, 'writeFileSync').throws(new Error('writeFileSync failed'));

await assert.rejects(command.action(logger, { options: {} }),
new CommandError(`Could not find Node version for 99.99.99 of SharePoint Framework`));
});

it('handles unexpected error', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));

Expand All @@ -177,9 +245,45 @@ describe(commands.PROJECT_GITHUB_WORKFLOW_ADD, () => {
return false;
});

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

sinon.stub(fs, 'writeFileSync').callsFake(() => {
throw new Error('writeFileSync failed');
});

await assert.rejects(command.action(logger, { options: {} } as any),
new CommandError('writeFileSync failed'));
});

it('handles unexpected non-error value', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), projectPath));

sinon.stub(fs, 'readFileSync').callsFake((path, options) => {
if (path.toString().endsWith('package.json') && options === 'utf-8') {
return '{"name": "test"}';
}

return '';
});

sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
if (fakePath.toString().endsWith('.github')) {
return true;
}
else if (fakePath.toString().endsWith('workflows')) {
return true;
}

return false;
});

sinon.stub(command as any, 'getProjectVersion').returns('1.21.1');

sinon.stub(fs, 'writeFileSync').callsFake(() => {
throw 'string failure';
});

await assert.rejects(command.action(logger, { options: {} } as any),
new CommandError('error'));
new CommandError('string failure'));
});
});
});
Loading