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
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,49 @@ describe(commands.PROJECT_AZUREDEVOPS_PIPELINE_ADD, () => {
assert(writeFileSyncStub.calledWith(path.resolve(path.join(projectPath, '.azuredevops', 'pipelines', 'deploy-spfx-solution.yml'))), 'workflow file not created');
});

it('creates a pipeline with heft for SPFx version that requires heft', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(projectPath);
sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
if (fakePath.toString() === path.join(projectPath, 'package.json')) {
return true;
}
else if (fakePath.toString() === path.join(projectPath, 'config', 'package-solution.json')) {
return true;
}
else if (fakePath.toString() === path.join(projectPath, '.azuredevops')) {
return true;
}
else if (fakePath.toString() === path.join(projectPath, '.azuredevops', 'pipelines')) {
return true;
}

throw `Invalid path: ${fakePath}`;
});

sinon.stub(fs, 'readFileSync').callsFake((filePath, options) => {
if (filePath.toString() === path.join(projectPath, 'package.json') && options === 'utf-8') {
return '{"name": "test"}';
}
else if (filePath.toString() === path.join(projectPath, 'config', 'package-solution.json') && options === 'utf-8') {
return '{"paths": {"zippedPackage": "solution/test.sppkg"}}';
}

throw `Invalid path: ${filePath}`;
});

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

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

await command.action(logger, { options: {} } as any);

assert(writeFileSyncStub.calledWith(path.resolve(path.join(projectPath, '.azuredevops', 'pipelines', 'deploy-spfx-solution.yml'))), 'workflow file not created');
const writtenContent: string = writeFileSyncStub.args[0][1] as string;
assert(writtenContent.includes('Heft build and package'), 'Heft step not found in pipeline');
assert(!writtenContent.includes('Gulp bundle'), 'Gulp bundle step should be removed');
assert(!writtenContent.includes('Gulp package'), 'Gulp package step should be removed');
});

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

Expand Down
25 changes: 25 additions & 0 deletions src/m365/spfx/commands/project/project-azuredevops-pipeline-add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,31 @@ class SpfxProjectAzureDevOpsPipelineAddCommand extends BaseProjectCommand {
if (options.skipFeatureDeployment) {
script.script = script.script.replace(`m365 spo app deploy `, `m365 spo app deploy --skipFeatureDeployment `);
}

if (versionRequirements.heft !== undefined) {
let steps = this.getPipelineSteps(pipeline);

const bundleStep = steps.find(step => step.task && step.displayName === "Gulp bundle");
if (bundleStep) {
pipeline.stages[0].jobs[0].steps = steps.filter(step => step !== bundleStep);
}

const packageStep = steps.find(step => step.task && step.displayName === "Gulp package");
if (packageStep) {
pipeline.stages[0].jobs[0].steps = pipeline.stages[0].jobs[0].steps.filter(step => step !== packageStep);
}

const installHeftStep: AzureDevOpsPipelineStep = {
task: "CmdLine@2",
displayName: "Heft build and package",
inputs: {
script: `npm install -g @rushstack/heft@latest\nheft build --production\nheft package-solution --production\n`
}
};
steps = this.getPipelineSteps(pipeline);
const installCLIStepIndex = steps.findIndex(step => step.task === "Npm@1" && step.displayName === "Install CLI for Microsoft 365");
pipeline.stages[0].jobs[0].steps.splice(installCLIStepIndex, 0, installHeftStep);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ export interface AzureDevOpsPipelineStep {
arguments?: string;
verbose?: boolean;
customCommand?: string;
script?: string;
}
}
47 changes: 47 additions & 0 deletions src/m365/spfx/commands/project/project-github-workflow-add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,53 @@ describe(commands.PROJECT_GITHUB_WORKFLOW_ADD, () => {
assert(writeFileSyncStub.calledWith(path.resolve(path.join(projectPath, '.github', 'workflows', 'deploy-spfx-solution.yml'))), 'workflow file not created');
});

it('uses heft build and package for newer version of SPFx', async () => {
sinon.stub(command as any, 'getProjectRoot').returns(projectPath);

sinon.stub(fs, 'existsSync').callsFake((fakePath) => {
if (fakePath.toString() === path.join(projectPath, '.github')) {
return true;
}
else if (fakePath.toString() === path.join(projectPath, '.github', 'workflows')) {
return true;
}
else if (fakePath.toString() === path.join(projectPath, 'package.json')) {
return true;
}
else if (fakePath.toString() === path.join(projectPath, '.yo-rc.json')) {
return true;
}
else if (fakePath.toString() === path.join(projectPath, 'config', 'package-solution.json')) {
return true;
}

return false;
});

sinon.stub(fs, 'readFileSync').callsFake((filePath, options) => {
if (filePath.toString() === path.join(projectPath, 'package.json') && options === 'utf-8') {
return '{"name": "test"}';
}
else if (filePath.toString() === path.join(projectPath, '.yo-rc.json') && options === 'utf-8') {
return '{"@microsoft/generator-sharepoint": {"version": "1.22.0"}}';
}
else if (filePath.toString() === path.join(projectPath, 'config', 'package-solution.json') && options === 'utf-8') {
return '{"paths": {"zippedPackage": "solution/test.sppkg"}}';
}

throw `Invalid path: ${filePath}`;
});

const writeFileSyncStub: sinon.SinonStub = sinon.stub(fs, 'writeFileSync').returns();

await command.action(logger, { options: {} } as any);

assert(writeFileSyncStub.calledOnce, 'writeFileSync not called');
const writtenContent: string = writeFileSyncStub.args[0][1];
assert(writtenContent.includes('heft build --production'), 'heft build not found in workflow');
assert(writtenContent.includes('heft package-solution --production'), 'heft package-solution not found in workflow');
});

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

Expand Down
11 changes: 11 additions & 0 deletions src/m365/spfx/commands/project/project-github-workflow-add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,23 @@ class SpfxProjectGithubWorkflowAddCommand extends BaseProjectCommand {
const deployAction = this.getDeployAction(workflow);
deployAction.with!.APP_FILE_PATH = deployAction.with!.APP_FILE_PATH!.replace('{{ sppkgPath }}', sppkgPath);
}

if (versionRequirements.heft !== undefined) {
const bundleAndPackageStep = this.getBundleAndPackageStep(workflow);
bundleAndPackageStep.run = `npm install -g @rushstack/heft@latest\nheft build --production\nheft package-solution --production\n`;
bundleAndPackageStep.name = 'Build & Package';
}
}

private assignNodeVersion(workflow: GitHubWorkflow, nodeVersion: string): void {
workflow.jobs['build-and-deploy'].env.NodeVersion = nodeVersion;
}

private getBundleAndPackageStep(workflow: GitHubWorkflow): GitHubWorkflowStep {
const steps = this.getWorkFlowSteps(workflow);
return steps.find(step => step.run && step.name === 'Bundle & Package')!;
}

private getLoginAction(workflow: GitHubWorkflow): GitHubWorkflowStep {
const steps = this.getWorkFlowSteps(workflow);
return steps.find(step => step.uses && step.uses.indexOf('action-cli-login') >= 0)!;
Expand Down
Loading